001package net.filebot.cli;
002
003import static java.nio.charset.StandardCharsets.*;
004import static java.util.Collections.*;
005import static java.util.stream.Collectors.*;
006import static net.filebot.Logging.*;
007import static net.filebot.MediaTypes.*;
008import static net.filebot.hash.VerificationUtilities.*;
009import static net.filebot.media.XattrMetaInfo.*;
010
011import java.awt.image.BufferedImage;
012import java.io.File;
013import java.io.FileFilter;
014import java.io.IOException;
015import java.net.URI;
016import java.net.URL;
017import java.nio.ByteBuffer;
018import java.nio.charset.Charset;
019import java.nio.file.FileVisitOption;
020import java.nio.file.FileVisitResult;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.nio.file.SimpleFileVisitor;
024import java.nio.file.attribute.BasicFileAttributes;
025import java.util.ArrayList;
026import java.util.Base64;
027import java.util.Collection;
028import java.util.EnumSet;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032
033import javax.imageio.ImageIO;
034import javax.imageio.stream.MemoryCacheImageInputStream;
035
036import org.codehaus.groovy.runtime.DefaultGroovyMethods;
037import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
038import org.imgscalr.Scalr;
039
040import groovy.lang.Closure;
041import groovy.lang.Range;
042import groovy.xml.StreamingMarkupBuilder;
043import groovy.xml.XmlSlurper;
044import groovy.xml.slurpersupport.GPathResult;
045
046import net.filebot.Cache;
047import net.filebot.CacheType;
048import net.filebot.MetaAttributeView;
049import net.filebot.RenameAction;
050import net.filebot.StandardRenameAction;
051import net.filebot.UserFiles;
052import net.filebot.format.AssociativeEnumObject;
053import net.filebot.format.ExpressionFormat;
054import net.filebot.format.FileSize;
055import net.filebot.format.MediaBindingBean;
056import net.filebot.media.CachedMediaCharacteristics;
057import net.filebot.media.FFProbe;
058import net.filebot.media.MediaCharacteristics;
059import net.filebot.media.MediaDetection;
060import net.filebot.media.MediaFileUtilities;
061import net.filebot.media.MediaInfoTable;
062import net.filebot.media.VideoQuality;
063import net.filebot.media.XattrChecksum;
064import net.filebot.similarity.NameSimilarityMetric;
065import net.filebot.similarity.Normalization;
066import net.filebot.similarity.SimilarityComparator;
067import net.filebot.util.ByteBufferInputStream;
068import net.filebot.util.ByteBufferOutputStream;
069import net.filebot.util.FileUtilities;
070import net.filebot.util.JsonUtilities;
071import net.filebot.util.XZ;
072import net.filebot.util.XmlUtilities;
073import net.filebot.web.Episode;
074import net.filebot.web.Movie;
075import net.filebot.web.OpenSubtitlesHasher;
076import net.filebot.web.WebRequest;
077
078public class ScriptShellMethods {
079
080        public static String getAt(File self, int index) {
081                List<File> path = FileUtilities.listPath(self);
082                File element = DefaultGroovyMethods.getAt(path, index);
083                return element == null ? null : index == 0 ? element.getPath() : element.getName();
084        }
085
086        public static File getAt(File self, Range range) {
087                List<File> path = FileUtilities.listPath(self);
088                return DefaultGroovyMethods.getAt(path, range).stream().reduce(null, (dir, f) -> {
089                        return dir == null && f == path.get(0) ? f : new File(dir, f.getName());
090                });
091        }
092
093        public static File getAt(File self, Collection indices) {
094                List<File> path = FileUtilities.listPath(self);
095                return DefaultGroovyMethods.getAt(path, indices).stream().reduce(null, (dir, f) -> {
096                        return dir == null && f == path.get(0) ? f : new File(dir, f.getName());
097                });
098        }
099
100        public static File resolve(File self, String path) {
101                File f = new File(path);
102                if (f.isAbsolute()) {
103                        return f;
104                }
105                return new File(self, f.getPath());
106        }
107
108        public static File resolveSibling(File self, String path) {
109                return resolve(self.getParentFile(), path);
110        }
111
112        public static File findSibling(File self, Closure<?> closure) throws Exception {
113                return MediaFileUtilities.findSiblingFiles(self, f -> {
114                        return DefaultTypeTransformation.castToBoolean(closure.call(f));
115                }).stream().findFirst().orElse(null);
116        }
117
118        public static List<File> listFiles(File self, Closure<?> closure) {
119                return FileUtilities.getChildren(self, f -> {
120                        return DefaultTypeTransformation.castToBoolean(closure.call(f));
121                }, FileUtilities.HUMAN_NAME_ORDER);
122        }
123
124        public static boolean isVideo(File self) {
125                return VIDEO_FILES.accept(self);
126        }
127
128        public static boolean isAudio(File self) {
129                return AUDIO_FILES.accept(self);
130        }
131
132        public static boolean isSubtitle(File self) {
133                return SUBTITLE_FILES.accept(self);
134        }
135
136        public static boolean isVerification(File self) {
137                return VERIFICATION_FILES.accept(self);
138        }
139
140        public static boolean isArchive(File self) {
141                return ARCHIVE_FILES.accept(self);
142        }
143
144        public static boolean isImage(File self) {
145                return IMAGE_FILES.accept(self);
146        }
147
148        public static boolean isDisk(File self) {
149                // check disk folder
150                if (MediaFileUtilities.isDiskFolder(self)) {
151                        return true;
152                }
153
154                // check disk image
155                if (self.isFile() && ISO.accept(self)) {
156                        try {
157                                return MediaFileUtilities.isVideoDiskFile(self);
158                        } catch (Exception e) {
159                                debug.warning(cause("Failed to read disk image", e));
160                        }
161                }
162
163                return false;
164        }
165
166        public static boolean isClutter(File self) {
167                return MediaFileUtilities.CLUTTER_TYPES.accept(self) || MediaFileUtilities.EXTRA_FOLDERS.accept(self) || MediaFileUtilities.EXTRA_FILES.accept(self);
168        }
169
170        public static boolean isSystem(File self) {
171                return MediaFileUtilities.SYSTEM_EXCLUDES.accept(self);
172        }
173
174        public static boolean isSymlink(File self) {
175                return Files.isSymbolicLink(self.toPath());
176        }
177
178        public static Object getAttribute(File self, String attribute) throws IOException {
179                return Files.getAttribute(self.toPath(), attribute);
180        }
181
182        public static Object getKey(File self) throws IOException {
183                return FileUtilities.getFileKey(self.getCanonicalFile());
184        }
185
186        public static int getLinkCount(File self) throws IOException {
187                return FileUtilities.getLinkCount(self);
188        }
189
190        public static boolean isNetworkDrive(File self) {
191                return FileUtilities.isNetworkDrive(self);
192        }
193
194        public static long getCreationDate(File self) throws IOException {
195                return FileUtilities.getCreationDate(self).toEpochMilli();
196        }
197
198        public static File getRealPath(File self) {
199                return FileUtilities.getRealPath(self);
200        }
201
202        public static List<File> getChildren(File self) {
203                return FileUtilities.getChildren(self, FileUtilities.NOT_HIDDEN, FileUtilities.HUMAN_NAME_ORDER);
204        }
205
206        public static File getDir(File self) {
207                return self.getParentFile();
208        }
209
210        public static boolean hasFile(File self, Closure<?> closure) {
211                return listFiles(self, closure).size() > 0;
212        }
213
214        public static List<File> getFiles(File self) {
215                return getFiles(singleton(self), null);
216        }
217
218        public static List<File> getFiles(File self, Closure<?> closure) {
219                return getFiles(singleton(self), closure);
220        }
221
222        public static List<File> getFiles(Collection<?> self) {
223                return getFiles(self, null);
224        }
225
226        public static List<File> getFiles(Collection<?> self, Closure<?> closure) {
227                List<File> roots = FileUtilities.asFileList(self.stream().distinct().toArray());
228                List<File> files = FileUtilities.listFiles(roots, FileUtilities.FILES, FileUtilities.HUMAN_NAME_ORDER);
229                if (closure != null) {
230                        files = DefaultGroovyMethods.findAll(files, closure);
231                }
232                return files;
233        }
234
235        public static List<File> getFolders(File self) {
236                return getFolders(self, null);
237        }
238
239        public static List<File> getFolders(File self, Closure<?> closure) {
240                return getFolders(singletonList(self), closure);
241        }
242
243        public static List<File> getFolders(Collection<?> self) {
244                return getFolders(self, null);
245        }
246
247        public static List<File> getFolders(Collection<?> self, Closure<?> closure) {
248                List<File> roots = FileUtilities.asFileList(self.toArray());
249                List<File> folders = FileUtilities.listFiles(roots, FileUtilities.FOLDERS, FileUtilities.HUMAN_NAME_ORDER);
250                if (closure != null) {
251                        folders = DefaultGroovyMethods.findAll(folders, closure);
252                }
253                return folders;
254        }
255
256        public static List<File> getMediaFolders(Collection<?> self) throws IOException {
257                List<File> folders = new ArrayList<File>();
258
259                for (File root : FileUtilities.asFileList(self.toArray())) {
260                        // resolve children for folder items
261                        if (root.isDirectory()) {
262                                Files.walkFileTree(root.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), FileUtilities.FILE_WALK_MAX_DEPTH, new SimpleFileVisitor<Path>() {
263                                        @Override
264                                        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
265                                                File folder = dir.toFile();
266
267                                                if (folder.isHidden() || !folder.canRead() || isSystem(folder) || isClutter(folder)) {
268                                                        return FileVisitResult.SKIP_SUBTREE;
269                                                }
270
271                                                if (FileUtilities.getChildren(folder, f -> isVideo(f) && !isClutter(f)).size() > 0 || MediaFileUtilities.isDiskFolder(folder)) {
272                                                        folders.add(folder);
273                                                        return FileVisitResult.SKIP_SUBTREE;
274                                                }
275
276                                                return FileVisitResult.CONTINUE;
277                                        }
278                                });
279                        }
280                        // resolve parent folder for video file items
281                        else if (root.getParentFile() != null && isVideo(root) && !isClutter(root)) {
282                                folders.add(root.getParentFile());
283                        }
284                }
285
286                return folders.stream().sorted().distinct().collect(toList());
287        }
288
289        public static void eachMediaFolder(Collection<?> self, Closure<?> closure) throws IOException {
290                DefaultGroovyMethods.each(getMediaFolders(self), closure);
291        }
292
293        public static String getNameWithoutExtension(File self) {
294                return FileUtilities.getNameWithoutExtension(self.getName());
295        }
296
297        public static String getNameWithoutExtension(String self) {
298                return FileUtilities.getNameWithoutExtension(self);
299        }
300
301        public static String getExtension(File self) {
302                return FileUtilities.getExtension(self);
303        }
304
305        public static String getExtension(String self) {
306                return FileUtilities.getExtension(self);
307        }
308
309        public static boolean hasExtension(File self, String... extensions) {
310                return FileUtilities.hasExtension(self, extensions);
311        }
312
313        public static boolean hasExtension(String self, String... extensions) {
314                return FileUtilities.hasExtension(self, extensions);
315        }
316
317        public static boolean isDerived(File self, File prime) {
318                return MediaFileUtilities.isDerived(self, prime) && !self.equals(prime); // a derived file cannot be the file itself
319        }
320
321        public static String validateFileName(String self) {
322                return FileUtilities.validateFileName(self);
323        }
324
325        public static File validateFilePath(File self) {
326                return FileUtilities.validateFilePath(self);
327        }
328
329        public static float getAge(File self) throws IOException {
330                return (System.currentTimeMillis() - getCreationDate(self)) / (24 * 60 * 60 * 1000f);
331        }
332
333        public static float getAgeLastModified(File self) throws IOException {
334                return (System.currentTimeMillis() - self.lastModified()) / (24 * 60 * 60 * 1000f);
335        }
336
337        public static FileSize getSize(File self) throws IOException {
338                return new FileSize(self.length());
339        }
340
341        public static String getDisplaySize(File self) {
342                return FileUtilities.formatSize(self.length());
343        }
344
345        public static String getDisplaySize(Number self) {
346                return FileUtilities.formatSize(self.longValue());
347        }
348
349        public static void createIfNotExists(File self) throws IOException {
350                if (!self.isFile()) {
351                        // create parent folder structure if necessary & create file
352                        Files.createDirectories(self.toPath().getParent());
353                        Files.createFile(self.toPath());
354                }
355        }
356
357        public static File relativize(File self, File other) throws IOException {
358                return FileUtilities.relativize(self, other);
359        }
360
361        public static Map<File, List<File>> mapByFolder(Collection<?> files) {
362                return FileUtilities.mapByFolder(FileUtilities.asFileList(files.toArray()));
363        }
364
365        public static Map<String, List<File>> mapByExtension(Collection<?> files) {
366                return FileUtilities.mapByExtension(FileUtilities.asFileList(files.toArray()));
367        }
368
369        public static String normalizePunctuation(String self) {
370                return Normalization.normalizePunctuation(self);
371        }
372
373        public static String stripReleaseInfo(String self, boolean strict) {
374                return MediaDetection.stripReleaseInfo(self, strict);
375        }
376
377        public static String getCRC32(File self) throws Exception {
378                return XattrChecksum.CRC32.computeIfAbsent(self);
379        }
380
381        public static String hash(File self, String hash) throws Exception {
382                switch (hash.toLowerCase(Locale.ROOT)) {
383                        case "moviehash":
384                                return OpenSubtitlesHasher.computeHash(self);
385                        case "crc32":
386                                return crc32(self);
387                        case "md5":
388                                return md5(self);
389                        case "sha256":
390                                return sha256(self);
391                }
392                throw new UnsupportedOperationException(hash);
393        }
394
395        public static long mismatch(File self, File other) throws Exception {
396                return FileUtilities.mismatch(self, other);
397        }
398
399        public static File move(File self, File to) throws Exception {
400                return call(StandardRenameAction.MOVE, self, to);
401        }
402
403        public static File copy(File self, File to) throws Exception {
404                return call(StandardRenameAction.COPY, self, to);
405        }
406
407        public static File duplicate(File self, File to) throws Exception {
408                return call(StandardRenameAction.DUPLICATE, self, to);
409        }
410
411        public static File call(RenameAction self, File from, File to) throws Exception {
412                // create parent folder structure
413                to = self.resolve(from, to);
414
415                // move files into the target directory using the current file name if the target file path is a directory
416                if (to.isAbsolute() && to.isDirectory()) {
417                        to = new File(to, from.getName());
418                }
419
420                // process files if the target file path is not already the current file path
421                if (self.canRename(from, to)) {
422                        return self.rename(from, to);
423                }
424
425                return null;
426        }
427
428        public static void trash(File self) throws Exception {
429                UserFiles.trash(self);
430        }
431
432        /*
433         * Web Request and File IO extensions
434         */
435
436        public static URL toURL(String self, Map<?, ?> parameters) throws Exception {
437                URI url = new URI(self);
438                String query = WebRequest.encodeParameters(parameters);
439                if (query.isEmpty()) {
440                        return url.toURL();
441                }
442                if (url.getQuery() == null) {
443                        return url.resolve(url.getPath() + "?" + query).toURL();
444                }
445                return url.resolve(url.getPath() + "?" + url.getQuery() + "&" + query).toURL();
446        }
447
448        public static URL div(URL self, String path) throws Exception {
449                return self.toURI().resolve(path).toURL();
450        }
451
452        public static String getText(ByteBuffer self) {
453                return UTF_8.decode(self.duplicate()).toString();
454        }
455
456        public static ByteBuffer encode(String self, String charset) throws IOException {
457                return Charset.forName(charset).encode(self);
458        }
459
460        public static ByteBuffer xz(ByteBuffer self) throws IOException {
461                return XZ.isXZ(self) ? self : XZ.xz(self.duplicate());
462        }
463
464        public static ByteBuffer unxz(ByteBuffer self) throws IOException {
465                return XZ.isXZ(self) ? XZ.unxz(self.duplicate()) : self;
466        }
467
468        public static ByteBuffer cache(URL self) throws Exception {
469                return Cache.getConcurrentCache(Cache.URL, CacheType.Monthly).url(self).transform(ByteBuffer::wrap).get();
470        }
471
472        public static BufferedImage getImage(URL self) throws Exception {
473                return getImage(cache(self));
474        }
475
476        public static BufferedImage getImage(ByteBuffer self) {
477                try {
478                        return ImageIO.read(new MemoryCacheImageInputStream(new ByteBufferInputStream(self.duplicate())));
479                } catch (Exception e) {
480                        debug.severe(cause(e));
481                }
482                return null;
483        }
484
485        public static BufferedImage scale(BufferedImage self, int width, int height) {
486                return Scalr.resize(self, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.AUTOMATIC, width, height);
487        }
488
489        public static File saveAs(BufferedImage self, File file) throws IOException {
490                return ImageIO.write(self, getExtension(file), file) ? file : null;
491        }
492
493        public static ByteBuffer encode(BufferedImage self, String format) throws IOException {
494                ByteBufferOutputStream buffer = new ByteBufferOutputStream(FileUtilities.BUFFER_SIZE);
495                return ImageIO.write(self, format, buffer) ? buffer.getByteBuffer() : null;
496        }
497
498        public static String base64(ByteBuffer self) throws IOException {
499                return getText(Base64.getEncoder().encode(self.duplicate()));
500        }
501
502        public static ByteBuffer fetch(URL self) throws IOException {
503                return WebRequest.fetch(self);
504        }
505
506        public static ByteBuffer get(URL self) throws IOException {
507                return WebRequest.fetch(self);
508        }
509
510        public static ByteBuffer get(URL self, Map<String, String> requestParameters) throws IOException {
511                return WebRequest.fetch(self, 0, null, requestParameters, null);
512        }
513
514        public static ByteBuffer post(URL self, Map<String, ?> parameters, Map<String, String> requestParameters) throws IOException {
515                return WebRequest.post(self, parameters, requestParameters);
516        }
517
518        public static ByteBuffer post(URL self, String text, Map<String, String> requestParameters) throws IOException {
519                return WebRequest.post(self, text.getBytes(UTF_8), "text/plain", requestParameters);
520        }
521
522        public static ByteBuffer post(URL self, byte[] postData, String contentType, Map<String, String> requestParameters) throws IOException {
523                return WebRequest.post(self, postData, contentType, requestParameters);
524        }
525
526        public static int head(URL self) throws IOException {
527                return WebRequest.status(WebRequest.HTTP_HEAD, self, null);
528        }
529
530        public static File saveAs(String self, String path) throws IOException {
531                return saveAs(UTF_8.encode(self), new File(path));
532        }
533
534        public static File saveAs(String self, File file) throws IOException {
535                return saveAs(UTF_8.encode(self), file);
536        }
537
538        public static File saveAs(URL self, String path) throws IOException {
539                return saveAs(WebRequest.fetch(self), new File(path));
540        }
541
542        public static File saveAs(URL self, File file) throws IOException {
543                return saveAs(WebRequest.fetch(self), file);
544        }
545
546        public static File saveAs(ByteBuffer self, String path) throws IOException {
547                return saveAs(self, new File(path));
548        }
549
550        public static File saveAs(ByteBuffer self, File file) throws IOException {
551                // resolve relative paths
552                file = file.getCanonicalFile();
553
554                // make sure parent folders exist
555                FileUtilities.createFolders(file.getParentFile());
556
557                return FileUtilities.writeFile(self, file);
558        }
559
560        public static GPathResult getXml(File self) throws Exception {
561                return new XmlSlurper().parse(self);
562        }
563
564        public static String serialize(GPathResult node) {
565                StreamingMarkupBuilder builder = new StreamingMarkupBuilder();
566                builder.setEncoding("UTF-8");
567                return builder.bindNode(node).toString();
568        }
569
570        public static File saveAs(GPathResult node, File file) throws Exception {
571                return XmlUtilities.writeDocument(WebRequest.getDocument(serialize(node)), file);
572        }
573
574        public static File getStructureRoot(File self) throws Exception {
575                return MediaFileUtilities.getStructureRoot(self);
576        }
577
578        public static File getStructurePathTail(File self) throws Exception {
579                return MediaFileUtilities.getStructurePathTail(self);
580        }
581
582        public static FolderWatchService watchFolder(File self, Closure<?> callback) {
583                // watch given folder non-recursively and collect events for 2s before processing changes
584                return watchFolder(self, false, 2000, f -> {
585                        // ignore deleted files, system files, hidden files, etc
586                        return FileUtilities.NOT_HIDDEN.accept(f) && (FileUtilities.FILES.accept(f) || FileUtilities.FOLDERS.accept(f));
587                }, callback);
588        }
589
590        public static FolderWatchService watchFolder(File self, boolean recursive, long delay, FileFilter filter, Closure<?> callback) {
591                FolderWatchService service = new FolderWatchService(recursive, delay, filter, callback::call);
592                service.watchFolder(self);
593                return service;
594        }
595
596        public static float getSimilarity(String self, String other) {
597                return new NameSimilarityMetric().getSimilarity(self, other);
598        }
599
600        public static Collection<?> sortBySimilarity(Collection<?> self, Object prime, Closure<String> mapper) {
601                return self.stream().sorted(SimilarityComparator.compareTo(prime.toString(), mapper == null ? Object::toString : mapper::call)).collect(toList());
602        }
603
604        public static boolean isBetter(File self, File other) {
605                if (VIDEO_FILES.accept(self) && VIDEO_FILES.accept(other)) {
606                        return VideoQuality.isBetter(self, other);
607                }
608                throw new UnsupportedOperationException("Compare [" + self + "] to [" + other + "]");
609        }
610
611        public static MetaAttributeView getXattr(File self) {
612                try {
613                        return new MetaAttributeView(self);
614                } catch (Exception e) {
615                        debug.severe(cause(e));
616                }
617                return null;
618        }
619
620        public static Object getMetadata(File self) {
621                try {
622                        return xattr.getMetaInfo(self);
623                } catch (Exception e) {
624                        debug.severe(cause(e));
625                }
626                return null;
627        }
628
629        public static void setMetadata(File self, Object object) {
630                try {
631                        xattr.setMetaInfo(self, object, null);
632                } catch (Exception e) {
633                        debug.severe(cause(e));
634                }
635        }
636
637        public static MediaCharacteristics getMediaCharacteristics(File self) {
638                return CachedMediaCharacteristics.getMediaCharacteristics(self).orElse(null);
639        }
640
641        public static Object getMediaInfo(File self) throws Exception {
642                return new AssociativeEnumObject(MediaInfoTable.read(self));
643        }
644
645        public static Object ffprobe(File self) throws Exception {
646                return new AssociativeEnumObject(FFProbe.read(self));
647        }
648
649        public static boolean isEpisode(File self) {
650                return MediaDetection.isEpisode(self, true);
651        }
652
653        public static boolean isMovie(File self) {
654                return MediaDetection.isMovie(self);
655        }
656
657        public static Object toJsonString(Object self) {
658                return JsonUtilities.json(self, true, false);
659        }
660
661        public static String apply(ExpressionFormat self, Object object) {
662                return self.format(new MediaBindingBean(object, null));
663        }
664
665        public static String apply(ExpressionFormat self, File file) {
666                return self.format(new MediaBindingBean(file, file));
667        }
668
669        public static String call(Movie self, String expression) throws Exception {
670                return new ExpressionFormat(expression).format(new MediaBindingBean(self, null));
671        }
672
673        public static String call(Episode self, String expression) throws Exception {
674                return new ExpressionFormat(expression).format(new MediaBindingBean(self, null));
675        }
676
677        public static String call(File self, String expression) throws Exception {
678                return new ExpressionFormat(expression).format(new MediaBindingBean(self, self));
679        }
680
681        private ScriptShellMethods() {
682                throw new UnsupportedOperationException();
683        }
684
685}