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