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