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.file.FileVisitOption;
019import java.nio.file.FileVisitResult;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.nio.file.SimpleFileVisitor;
023import java.nio.file.attribute.BasicFileAttributes;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.EnumSet;
027import java.util.List;
028import java.util.Map;
029
030import javax.imageio.ImageIO;
031import javax.imageio.stream.MemoryCacheImageInputStream;
032
033import org.codehaus.groovy.runtime.DefaultGroovyMethods;
034import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
035
036import groovy.lang.Closure;
037import groovy.lang.Range;
038
039import net.filebot.Cache;
040import net.filebot.CacheType;
041import net.filebot.CachedResource.Transform;
042import net.filebot.MetaAttributeView;
043import net.filebot.UserFiles;
044import net.filebot.format.AssociativeEnumObject;
045import net.filebot.format.ExpressionFormat;
046import net.filebot.format.FileSize;
047import net.filebot.format.MediaBindingBean;
048import net.filebot.media.CachedMediaCharacteristics;
049import net.filebot.media.FFProbe;
050import net.filebot.media.MediaCharacteristics;
051import net.filebot.media.MediaDetection;
052import net.filebot.media.MediaFileUtilities;
053import net.filebot.media.MetaAttributes;
054import net.filebot.media.XattrChecksum;
055import net.filebot.mediainfo.MediaInfo;
056import net.filebot.similarity.NameSimilarityMetric;
057import net.filebot.similarity.Normalization;
058import net.filebot.similarity.SimilarityComparator;
059import net.filebot.util.ByteBufferInputStream;
060import net.filebot.util.FileUtilities;
061import net.filebot.web.OpenSubtitlesHasher;
062import net.filebot.web.WebRequest;
063
064public class ScriptShellMethods {
065
066        public static String getAt(File self, int index) {
067                File path = DefaultGroovyMethods.getAt(FileUtilities.listPath(self), index);
068                return path == null ? null : path.getName();
069        }
070
071        public static File getAt(File self, Range<?> range) {
072                List<File> path = DefaultGroovyMethods.getAt(FileUtilities.listPath(self), range);
073                return new File(path.stream().map(File::getName).collect(joining(File.separator)));
074        }
075
076        public static File resolve(File self, String path) {
077                if (path == null || path.isEmpty()) {
078                        throw new IllegalArgumentException("Path is empty");
079                }
080                return new File(self, path);
081        }
082
083        public static File resolveSibling(File self, String path) {
084                return new File(self.getParentFile(), path);
085        }
086
087        public static File findSibling(File self, Closure<?> closure) throws Exception {
088                return MediaFileUtilities.findSiblingFiles(self, f -> {
089                        return DefaultTypeTransformation.castToBoolean(closure.call(f));
090                }).stream().findFirst().orElse(null);
091        }
092
093        public static List<File> listFiles(File self, Closure<?> closure) {
094                return FileUtilities.getChildren(self, f -> {
095                        return DefaultTypeTransformation.castToBoolean(closure.call(f));
096                }, FileUtilities.HUMAN_NAME_ORDER);
097        }
098
099        public static boolean isVideo(File self) {
100                return VIDEO_FILES.accept(self);
101        }
102
103        public static boolean isAudio(File self) {
104                return AUDIO_FILES.accept(self);
105        }
106
107        public static boolean isSubtitle(File self) {
108                return SUBTITLE_FILES.accept(self);
109        }
110
111        public static boolean isVerification(File self) {
112                return VERIFICATION_FILES.accept(self);
113        }
114
115        public static boolean isArchive(File self) {
116                return ARCHIVE_FILES.accept(self);
117        }
118
119        public static boolean isImage(File self) {
120                return IMAGE_FILES.accept(self);
121        }
122
123        public static boolean isDisk(File self) {
124                // check disk folder
125                if (MediaFileUtilities.isDiskFolder(self)) {
126                        return true;
127                }
128
129                // check disk image
130                if (self.isFile() && ISO.accept(self)) {
131                        try {
132                                return MediaFileUtilities.isVideoDiskFile(self);
133                        } catch (Exception e) {
134                                debug.warning(cause("Failed to read disk image", e));
135                        }
136                }
137
138                return false;
139        }
140
141        public static boolean isClutter(File self) {
142                return MediaFileUtilities.CLUTTER_TYPES.accept(self) || MediaFileUtilities.EXTRA_FOLDERS.accept(self) || MediaFileUtilities.EXTRA_FILES.accept(self);
143        }
144
145        public static boolean isSystem(File self) {
146                return MediaFileUtilities.SYSTEM_EXCLUDES.accept(self);
147        }
148
149        public static boolean isSymlink(File self) {
150                return Files.isSymbolicLink(self.toPath());
151        }
152
153        public static Object getAttribute(File self, String attribute) throws IOException {
154                return Files.getAttribute(self.toPath(), attribute);
155        }
156
157        public static Object getKey(File self) throws IOException {
158                return FileUtilities.getFileKey(self.getCanonicalFile());
159        }
160
161        public static int getLinkCount(File self) throws IOException {
162                return FileUtilities.getLinkCount(self);
163        }
164
165        public static long getCreationDate(File self) throws IOException {
166                return FileUtilities.getCreationDate(self).toEpochMilli();
167        }
168
169        public static File getDir(File self) {
170                return self.getParentFile();
171        }
172
173        public static boolean hasFile(File self, Closure<?> closure) {
174                return listFiles(self, closure).size() > 0;
175        }
176
177        public static List<File> getFiles(File self) {
178                return getFiles(self, null);
179        }
180
181        public static List<File> getFiles(File self, Closure<?> closure) {
182                return getFiles(singleton(self), closure);
183        }
184
185        public static List<File> getFiles(Collection<?> self) {
186                return getFiles(self, null);
187        }
188
189        public static List<File> getFiles(Collection<?> self, Closure<?> closure) {
190                List<File> roots = FileUtilities.asFileList(self.toArray());
191                List<File> files = FileUtilities.listFiles(roots, FileUtilities.FILES, FileUtilities.HUMAN_NAME_ORDER);
192                if (closure != null) {
193                        files = DefaultGroovyMethods.findAll(files, closure);
194                }
195                return files;
196        }
197
198        public static List<File> getFolders(File self) {
199                return getFolders(self, null);
200        }
201
202        public static List<File> getFolders(File self, Closure<?> closure) {
203                return getFolders(singletonList(self), closure);
204        }
205
206        public static List<File> getFolders(Collection<?> self) {
207                return getFolders(self, null);
208        }
209
210        public static List<File> getFolders(Collection<?> self, Closure<?> closure) {
211                List<File> roots = FileUtilities.asFileList(self.toArray());
212                List<File> folders = FileUtilities.listFiles(roots, FileUtilities.FOLDERS, FileUtilities.HUMAN_NAME_ORDER);
213                if (closure != null) {
214                        folders = DefaultGroovyMethods.findAll(folders, closure);
215                }
216                return folders;
217        }
218
219        public static List<File> getMediaFolders(Collection<?> self) throws IOException {
220                List<File> folders = new ArrayList<File>();
221
222                for (File root : FileUtilities.asFileList(self.toArray())) {
223                        // resolve children for folder items
224                        if (root.isDirectory()) {
225                                Files.walkFileTree(root.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), FileUtilities.FILE_WALK_MAX_DEPTH, new SimpleFileVisitor<Path>() {
226                                        @Override
227                                        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
228                                                File folder = dir.toFile();
229
230                                                if (folder.isHidden() || !folder.canRead() || isSystem(folder) || isClutter(folder)) {
231                                                        return FileVisitResult.SKIP_SUBTREE;
232                                                }
233
234                                                if (FileUtilities.getChildren(folder, f -> isVideo(f) && !isClutter(f)).size() > 0 || MediaFileUtilities.isDiskFolder(folder)) {
235                                                        folders.add(folder);
236                                                        return FileVisitResult.SKIP_SUBTREE;
237                                                }
238
239                                                return FileVisitResult.CONTINUE;
240                                        }
241                                });
242                        }
243                        // resolve parent folder for video file items
244                        else if (root.getParentFile() != null && isVideo(root) && !isClutter(root)) {
245                                folders.add(root.getParentFile());
246                        }
247                }
248
249                return folders.stream().sorted().distinct().collect(toList());
250        }
251
252        public static void eachMediaFolder(Collection<?> self, Closure<?> closure) throws IOException {
253                DefaultGroovyMethods.each(getMediaFolders(self), closure);
254        }
255
256        public static String getNameWithoutExtension(File self) {
257                return FileUtilities.getNameWithoutExtension(self.getName());
258        }
259
260        public static String getNameWithoutExtension(String self) {
261                return FileUtilities.getNameWithoutExtension(self);
262        }
263
264        public static String getExtension(File self) {
265                return FileUtilities.getExtension(self);
266        }
267
268        public static String getExtension(String self) {
269                return FileUtilities.getExtension(self);
270        }
271
272        public static boolean hasExtension(File self, String... extensions) {
273                return FileUtilities.hasExtension(self, extensions);
274        }
275
276        public static boolean hasExtension(String self, String... extensions) {
277                return FileUtilities.hasExtension(self, extensions);
278        }
279
280        public static boolean isDerived(File self, File other) {
281                return MediaFileUtilities.isDerived(self, other);
282        }
283
284        public static String validateFileName(String self) {
285                return FileUtilities.validateFileName(self);
286        }
287
288        public static File validateFilePath(File self) {
289                return FileUtilities.validateFilePath(self);
290        }
291
292        public static File moveTo(File self, File destination) throws IOException {
293                return FileUtilities.moveRename(self, destination);
294        }
295
296        public static File copyAs(File self, File destination) throws IOException {
297                return FileUtilities.copyAs(self, destination);
298        }
299
300        public static File copyTo(File self, File destination) throws IOException {
301                return FileUtilities.copyAs(self, new File(destination, self.getName()));
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 self.getCanonicalFile().toPath().relativize(other.getCanonicalFile().toPath()).toFile();
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) {
353                return XattrChecksum.CRC32.computeIfAbsent(self);
354        }
355
356        public static String getMD5(File self) {
357                return XattrChecksum.MD5.computeIfAbsent(self);
358        }
359
360        public static String hash(File self, String hash) throws Exception {
361                switch (hash.toLowerCase()) {
362                        case "moviehash":
363                                return OpenSubtitlesHasher.computeHash(self);
364                        case "crc32":
365                                return crc32(self);
366                        case "md5":
367                                return md5(self);
368                        case "sha256":
369                                return sha256(self);
370                }
371                throw new IllegalArgumentException(hash);
372        }
373
374        public static void trash(File self) throws Exception {
375                UserFiles.trash(self);
376        }
377
378        /*
379         * Web Request and File IO extensions
380         */
381
382        public static URL toURL(String self, Map<?, ?> parameters) throws Exception {
383                URL url = new URL(self);
384                String query = WebRequest.encodeParameters(parameters);
385                if (query.isEmpty()) {
386                        return url;
387                }
388                if (url.getQuery() == null) {
389                        return new URL(url, url.getPath() + "?" + query);
390                }
391                return new URL(url, url.getFile() + "&" + query);
392        }
393
394        public static ByteBuffer cache(URL self) throws Exception {
395                Cache cache = Cache.getConcurrentCache(Cache.URL, CacheType.Monthly);
396                byte[] bytes = cache.bytes(self.toExternalForm(), URL::new).get();
397                return ByteBuffer.wrap(bytes);
398        }
399
400        public static <R> R cache(URL self, Transform<InputStream, R> processor) throws Exception {
401                Cache cache = Cache.getConcurrentCache(Cache.URL, CacheType.Monthly);
402                return cache.stream(self.toExternalForm(), URL::new, processor).get();
403        }
404
405        public static String getText(ByteBuffer self) {
406                return UTF_8.decode(self.duplicate()).toString();
407        }
408
409        public static RenderedImage getImage(ByteBuffer self) {
410                try {
411                        return ImageIO.read(new MemoryCacheImageInputStream(new ByteBufferInputStream(self.duplicate())));
412                } catch (Exception e) {
413                        debug.warning(e::toString);
414                }
415                return null;
416        }
417
418        public static File saveAs(RenderedImage self, File file) throws IOException {
419                return ImageIO.write(self, getExtension(file), file) ? file : null;
420        }
421
422        public static ByteBuffer fetch(URL self) throws IOException {
423                return WebRequest.fetch(self);
424        }
425
426        public static ByteBuffer get(URL self) throws IOException {
427                return WebRequest.fetch(self);
428        }
429
430        public static ByteBuffer get(URL self, Map<String, String> requestParameters) throws IOException {
431                return WebRequest.fetch(self, 0, null, requestParameters, null);
432        }
433
434        public static ByteBuffer post(URL self, Map<String, ?> parameters, Map<String, String> requestParameters) throws IOException {
435                return WebRequest.post(self, parameters, requestParameters);
436        }
437
438        public static ByteBuffer post(URL self, String text, Map<String, String> requestParameters) throws IOException {
439                return WebRequest.post(self, text.getBytes(UTF_8), "text/plain", requestParameters);
440        }
441
442        public static ByteBuffer post(URL self, byte[] postData, String contentType, Map<String, String> requestParameters) throws IOException {
443                return WebRequest.post(self, postData, contentType, requestParameters);
444        }
445
446        public static int head(URL self) throws IOException {
447                return WebRequest.head(self);
448        }
449
450        public static File saveAs(String self, String path) throws IOException {
451                return saveAs(UTF_8.encode(self), new File(path));
452        }
453
454        public static File saveAs(String self, File file) throws IOException {
455                return saveAs(UTF_8.encode(self), file);
456        }
457
458        public static File saveAs(URL self, String path) throws IOException {
459                return saveAs(WebRequest.fetch(self), new File(path));
460        }
461
462        public static File saveAs(URL self, File file) throws IOException {
463                return saveAs(WebRequest.fetch(self), file);
464        }
465
466        public static File saveAs(ByteBuffer self, String path) throws IOException {
467                return saveAs(self, new File(path));
468        }
469
470        public static File saveAs(ByteBuffer self, File file) throws IOException {
471                // resolve relative paths
472                file = file.getCanonicalFile();
473
474                // make sure parent folders exist
475                FileUtilities.createFolders(file.getParentFile());
476
477                return FileUtilities.writeFile(self, file);
478        }
479
480        public static File getStructureRoot(File self) throws Exception {
481                return MediaFileUtilities.getStructureRoot(self);
482        }
483
484        public static File getStructurePathTail(File self) throws Exception {
485                return MediaFileUtilities.getStructurePathTail(self);
486        }
487
488        public static FolderWatchService watchFolder(File self, Closure<?> callback) {
489                // watch given folder non-recursively and collect events for 2s before processing changes
490                return watchFolder(self, false, 2000, f -> {
491                        // ignore deleted files, system files, hidden files, etc
492                        return FileUtilities.NOT_HIDDEN.accept(f) && (FileUtilities.FILES.accept(f) || FileUtilities.FOLDERS.accept(f));
493                }, callback);
494        }
495
496        public static FolderWatchService watchFolder(File self, boolean recursive, long delay, FileFilter filter, Closure<?> callback) {
497                FolderWatchService service = new FolderWatchService(recursive, delay, filter, callback::call);
498                service.watchFolder(self);
499                return service;
500        }
501
502        public static float getSimilarity(String self, String other) {
503                return new NameSimilarityMetric().getSimilarity(self, other);
504        }
505
506        public static Collection<?> sortBySimilarity(Collection<?> self, Object prime, Closure<String> mapper) {
507                return self.stream().sorted(SimilarityComparator.compareTo(prime.toString(), mapper == null ? Object::toString : mapper::call)).collect(toList());
508        }
509
510        public static MetaAttributeView getXattr(File self) {
511                try {
512                        return new MetaAttributeView(self);
513                } catch (Exception e) {
514                        debug.warning(e::toString);
515                }
516                return null;
517        }
518
519        public static Object getMetadata(File self) {
520                try {
521                        return xattr.getMetaInfo(self);
522                } catch (Exception e) {
523                        debug.warning(e::toString);
524                }
525                return null;
526        }
527
528        public static void setMetadata(File self, Object object) {
529                try {
530                        xattr.setMetaInfo(self, object, null);
531                } catch (Exception e) {
532                        debug.warning(e::toString);
533                }
534        }
535
536        public static MediaCharacteristics getMediaCharacteristics(File self) {
537                return CachedMediaCharacteristics.getMediaCharacteristics(self).orElse(null);
538        }
539
540        public static Object getMediaInfo(File self) throws Exception {
541                return new AssociativeEnumObject(MediaInfo.snapshot(self));
542        }
543
544        public static Object ffprobe(File self) throws Exception {
545                return new AssociativeEnumObject(FFProbe.snapshot(self));
546        }
547
548        public static boolean isEpisode(File self) {
549                return MediaDetection.isEpisode(self, true);
550        }
551
552        public static boolean isMovie(File self) {
553                return MediaDetection.isMovie(self);
554        }
555
556        public static Object toJsonString(Object self) {
557                return MetaAttributes.toJson(self, false);
558        }
559
560        public static String apply(ExpressionFormat self, Object object) {
561                return self.format(new MediaBindingBean(object, null));
562        }
563
564        private ScriptShellMethods() {
565                throw new UnsupportedOperationException();
566        }
567
568}