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