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