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}