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