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