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