diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy index 822a868e..1ca7754e 100644 --- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy +++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy @@ -8,8 +8,9 @@ File.metaClass.isVideo = { _types.getFilter("video").accept(delegate) } File.metaClass.isAudio = { _types.getFilter("audio").accept(delegate) } File.metaClass.isSubtitle = { _types.getFilter("subtitle").accept(delegate) } File.metaClass.isVerification = { _types.getFilter("verification").accept(delegate) } +File.metaClass.isArchive = { _types.getFilter("archive").accept(delegate) } -File.metaClass.dir = { getParentFile() } +File.metaClass.getDir = { getParentFile() } File.metaClass.hasFile = { c -> isDirectory() && listFiles().find(c) } String.metaClass.getFiles = { c -> new File(delegate).getFiles(c) } @@ -38,6 +39,8 @@ File.metaClass.validateFilePath = { validateFilePath(delegate) } File.metaClass.moveTo = { f -> renameFile(delegate, f) } List.metaClass.mapByFolder = { mapByFolder(delegate) } List.metaClass.mapByExtension = { mapByExtension(delegate) } +String.metaClass.getExtension = { getExtension(delegate) } +String.metaClass.hasExtension = { String... ext -> hasExtension(delegate, ext) } // WebRequest utility methods @@ -78,19 +81,20 @@ def getWatchService(Closure callback, List folders) { folders.find{ if (!it.isDirectory()) throw new Exception("Must be a folder: " + it) } // create watch service and setup callback - def watchService = new FolderWatchService() { + def watchService = new FolderWatchService(true) { @Override - def void processCommitSet(File[] fileset) { + def void processCommitSet(File[] fileset, File dir) { callback(fileset.toList()) } } // collect updates for 5 minutes and then batch process watchService.setCommitDelay(5 * 60 * 1000) + watchService.setCommitPerFolder(true) // start watching given files - folders.each { watchService.watch(it) } + folders.each { dir -> _guarded { watchService.watchFolder(dir) } } return watchService } @@ -99,6 +103,22 @@ File.metaClass.watch = { c -> getWatchService(c, [delegate]) } List.metaClass.watch = { c -> getWatchService(c, delegate) } +// Season / Episode helpers +import net.sourceforge.filebot.mediainfo.ReleaseInfo +import net.sourceforge.filebot.similarity.SeasonEpisodeMatcher + +def guessEpisodeNumber(path) { + def input = path instanceof File ? path.getName() : path.toString() + def sxe = new SeasonEpisodeMatcher(new SeasonEpisodeMatcher.SeasonEpisodeFilter(30, 50, 1000)).match(input) + return sxe == null || sxe.isEmpty() ? null : sxe[0] +} + +def detectSeriesName(files) { + def names = ReleaseInfo.detectSeriesNames(files.findAll { it.isVideo() || it.isSubtitle() }) + return names == null || names.isEmpty() ? null : names[0] +} + + // CLI bindings def rename(args) { args = _defaults(args) diff --git a/source/net/sourceforge/filebot/web/TheTVDBClient.java b/source/net/sourceforge/filebot/web/TheTVDBClient.java index cec2aa92..bb6dfe30 100644 --- a/source/net/sourceforge/filebot/web/TheTVDBClient.java +++ b/source/net/sourceforge/filebot/web/TheTVDBClient.java @@ -7,6 +7,7 @@ import static net.sourceforge.filebot.web.WebRequest.*; import static net.sourceforge.tuned.XPathUtilities.*; import java.io.FileNotFoundException; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.ArrayList; @@ -30,6 +31,8 @@ import org.w3c.dom.Node; import net.sf.ehcache.CacheManager; import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.web.TheTVDBClient.BannerDescriptor.BannerProperty; +import net.sourceforge.tuned.FileUtilities; public class TheTVDBClient extends AbstractEpisodeListProvider { @@ -367,21 +370,11 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { * * @see http://thetvdb.com/wiki/index.php/API:banners.xml */ - public Map getBanner(TheTVDBSearchResult series, String bannerType, String bannerType2, Integer season, String language) throws Exception { - // build selector - Map selector = new EnumMap(BannerProperty.class); - if (bannerType != null) - selector.put(BannerProperty.BannerType, bannerType); - if (bannerType2 != null) - selector.put(BannerProperty.BannerType2, bannerType2); - if (season != null) - selector.put(BannerProperty.Season, new Double(season)); - if (language != null) - selector.put(BannerProperty.Language, language); - + public BannerDescriptor getBanner(TheTVDBSearchResult series, String bannerType, String bannerType2, Integer season, Locale locale) throws Exception { // search for a banner matching the selector - for (Map it : getBannerDescriptor(series.seriesId)) { - if (it.entrySet().containsAll(selector.entrySet())) { + for (BannerDescriptor it : getBannerList(series.seriesId)) { + if ((bannerType == null || it.getBannerType().equalsIgnoreCase(bannerType)) && (bannerType2 == null || it.getBannerType2().equalsIgnoreCase(bannerType2)) && (season == null || it.getSeason().equals(season)) + && ((locale == null && it.getLocale().getLanguage().equals("en")) || it.getLocale().getLanguage().equals(locale.getLanguage()))) { return it; } } @@ -390,15 +383,18 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } - public List> getBannerDescriptor(int seriesid) throws Exception { + public List getBannerList(int seriesid) throws Exception { Document dom = getDocument(getResource(MirrorType.XML, "/api/" + apikey + "/series/" + seriesid + "/banners.xml")); List nodes = selectNodes("//Banner", dom); - List> banners = new ArrayList>(); + List banners = new ArrayList(); for (Node node : nodes) { try { - EnumMap item = new EnumMap(BannerProperty.class); + EnumMap item = new EnumMap(BannerProperty.class); + + // insert banner mirror + item.put(BannerProperty.BannerMirror, getResource(MirrorType.BANNER, "/banners/").toString()); // copy values from xml for (BannerProperty key : BannerProperty.values()) { @@ -408,21 +404,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } } - // parse numbers - for (BannerProperty key : BannerProperty.numbers()) { - if (item.get(key) != null) { - item.put(key, new Double(item.get(key).toString())); - } - } - - // resolve relative urls - for (BannerProperty key : BannerProperty.urls()) { - if (item.get(key) != null) { - item.put(key, getResource(MirrorType.BANNER, "/banners/" + item.get(key))); - } - } - - banners.add(item); + banners.add(new BannerDescriptor(item)); } catch (Exception e) { // log and ignore Logger.getLogger(getClass().getName()).log(Level.WARNING, "Invalid banner descriptor", e); @@ -433,27 +415,110 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } - public static enum BannerProperty { - id, - BannerPath, - BannerType, - BannerType2, - Season, - Colors, - Language, - Rating, - RatingCount, - SeriesName, - ThumbnailPath, - VignettePath; + public static class BannerDescriptor { - public static BannerProperty[] numbers() { - return new BannerProperty[] { id, Season, Rating, RatingCount }; + public static enum BannerProperty { + id, + BannerMirror, + BannerPath, + BannerType, + BannerType2, + Season, + Colors, + Language, + Rating, + RatingCount, + SeriesName, + ThumbnailPath, + VignettePath } - public static BannerProperty[] urls() { - return new BannerProperty[] { BannerPath, ThumbnailPath, VignettePath }; + private EnumMap fields; + + + protected BannerDescriptor(Map fields) { + this.fields = new EnumMap(fields); + } + + + public URL getMirrorUrl() throws MalformedURLException { + return new URL(fields.get(BannerProperty.BannerMirror)); + } + + + public URL getUrl() throws MalformedURLException { + return new URL(getMirrorUrl(), fields.get(BannerProperty.BannerPath)); + } + + + public String getExtension() { + return FileUtilities.getExtension(fields.get(BannerProperty.BannerPath)); + } + + + public int getId() { + return Integer.parseInt(fields.get(BannerProperty.id)); + } + + + public String getBannerType() { + return fields.get(BannerProperty.BannerType); + } + + + public String getBannerType2() { + return fields.get(BannerProperty.BannerType2); + } + + + public Integer getSeason() { + try { + return new Integer(fields.get(BannerProperty.Season)); + } catch (NumberFormatException e) { + return null; + } + } + + + public String getColors() { + return fields.get(BannerProperty.Colors); + } + + + public Locale getLocale() { + return new Locale(fields.get(BannerProperty.Language)); + } + + + public double getRating() { + return Double.parseDouble(fields.get(BannerProperty.Rating)); + } + + + public int getRatingCount() { + return Integer.parseInt(fields.get(BannerProperty.RatingCount)); + } + + + public boolean hasSeriesName() { + return Boolean.parseBoolean(fields.get(BannerProperty.SeriesName)); + } + + + public URL getThumbnailUrl() throws MalformedURLException { + return new URL(getMirrorUrl(), fields.get(BannerProperty.ThumbnailPath)); + } + + + public URL getVignetteUrl() throws MalformedURLException { + return new URL(getMirrorUrl(), fields.get(BannerProperty.VignettePath)); + } + + + @Override + public String toString() { + return fields.toString(); } } diff --git a/test/net/sourceforge/filebot/web/TheTVDBClientTest.java b/test/net/sourceforge/filebot/web/TheTVDBClientTest.java index 9b0f5e74..c7a5127a 100644 --- a/test/net/sourceforge/filebot/web/TheTVDBClientTest.java +++ b/test/net/sourceforge/filebot/web/TheTVDBClientTest.java @@ -4,18 +4,16 @@ package net.sourceforge.filebot.web; import static org.junit.Assert.*; -import java.net.URL; import java.util.EnumSet; import java.util.List; import java.util.Locale; -import java.util.Map; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import net.sf.ehcache.CacheManager; -import net.sourceforge.filebot.web.TheTVDBClient.BannerProperty; +import net.sourceforge.filebot.web.TheTVDBClient.BannerDescriptor; import net.sourceforge.filebot.web.TheTVDBClient.MirrorType; import net.sourceforge.filebot.web.TheTVDBClient.TheTVDBSearchResult; @@ -150,24 +148,24 @@ public class TheTVDBClientTest { @Test public void getBanner() throws Exception { - Map banner = thetvdb.getBanner(new TheTVDBSearchResult("Buffy the Vampire Slayer", 70327), "season", "seasonwide", 7, "en"); + BannerDescriptor banner = thetvdb.getBanner(new TheTVDBSearchResult("Buffy the Vampire Slayer", 70327), "season", "seasonwide", 7, Locale.ENGLISH); - assertEquals(857660, (Double) banner.get(BannerProperty.id), 0); - assertEquals("season", banner.get(BannerProperty.BannerType)); - assertEquals("seasonwide", banner.get(BannerProperty.BannerType2)); - assertEquals("http://thetvdb.com/banners/seasonswide/70327-7.jpg", banner.get(BannerProperty.BannerPath).toString()); - assertEquals(99712, WebRequest.fetch((URL) banner.get(BannerProperty.BannerPath)).remaining(), 0); + assertEquals(857660, banner.getId(), 0); + assertEquals("season", banner.getBannerType()); + assertEquals("seasonwide", banner.getBannerType2()); + assertEquals("http://thetvdb.com/banners/seasonswide/70327-7.jpg", banner.getUrl().toString()); + assertEquals(99712, WebRequest.fetch(banner.getUrl()).remaining(), 0); } @Test - public void getBannerDescriptor() throws Exception { - List> banners = thetvdb.getBannerDescriptor(70327); + public void getBannerList() throws Exception { + List banners = thetvdb.getBannerList(70327); assertEquals(106, banners.size()); - assertEquals("fanart", banners.get(0).get(BannerProperty.BannerType)); - assertEquals("1280x720", banners.get(0).get(BannerProperty.BannerType2)); - assertEquals(486993, WebRequest.fetch((URL) banners.get(0).get(BannerProperty.BannerPath)).remaining(), 0); + assertEquals("fanart", banners.get(0).getBannerType()); + assertEquals("1280x720", banners.get(0).getBannerType2()); + assertEquals(486993, WebRequest.fetch(banners.get(0).getUrl()).remaining(), 0); } diff --git a/website/data/shell/banners.groovy b/website/data/shell/banners.groovy new file mode 100644 index 00000000..f47ce3e0 --- /dev/null +++ b/website/data/shell/banners.groovy @@ -0,0 +1,52 @@ +// filebot -script "http://filebot.sourceforge.net/data/shell/banners.groovy" -trust-script /path/to/media/ + +/* + * Fetch series and season banners for all tv shows + */ +import static net.sourceforge.filebot.WebServices.* + + +def fetchBanner(dir, series, bannerType, bannerType2, season = null) { + def name = "$series $bannerType ${season ? 'S'+season : 'all'} $bannerType2".space('.') + + // select and fetch banner + def banner = TheTVDB.getBanner(series, bannerType, bannerType2, season, Locale.ENGLISH) + if (banner == null) { + println "Banner not found: $name" + return; + } + + println "Fetching $name" + banner.url.saveAs(new File(dir, name + "." + banner.extension)) +} + + +def fetchSeriesBanners(dir, series, seasons) { + println "Fetch banners for $series / Season $seasons" + + // fetch series banner + fetchBanner(dir, series, "series", "graphical") + + // fetch season banners + seasons.each { s -> + fetchBanner(dir, series, "season", "season", s) + fetchBanner(dir, series, "season", "seasonwide", s) + } +} + + +args.eachMediaFolder() { dir -> + println "Processing $dir" + def videoFiles = dir.listFiles{ it.isVideo() } + + def seriesName = detectSeriesName(videoFiles) + def seasons = videoFiles.findResults { guessEpisodeNumber(it)?.season }.unique() + + def options = TheTVDB.search(seriesName) + if (options.isEmpty()) { + println "TV Series not found: $name" + return; + } + + fetchSeriesBanners(dir, options[0], seasons) +} diff --git a/website/data/shell/cleaner.groovy b/website/data/shell/cleaner.groovy new file mode 100644 index 00000000..5f5f1bf3 --- /dev/null +++ b/website/data/shell/cleaner.groovy @@ -0,0 +1,18 @@ +// filebot -script "http://filebot.sourceforge.net/data/shell/cleaner.groovy" -trust-script /path/to/media/ + +/* + * Delete orphaned "clutter" files like nfo, jpg, etc + */ +def isClutter(file) { + return file.hasExtension("nfo", "txt", "jpg", "jpeg") +} + +// delete clutter files in orphaned media folders +args.getFiles{ isClutter(it) && !it.dir.hasFile{ it.isVideo() }}.each { + println "Delete file $it: " + it.delete() +} + +// delete empty folders but exclude roots +args.getFolders{ it.getFiles().isEmpty() && !args.contains(it) }.each { + println "Delete dir $it: " + it.deleteDir() +} diff --git a/website/script.html b/website/script.html index b3a6825a..e05ce3e4 100644 --- a/website/script.html +++ b/website/script.html @@ -94,7 +94,7 @@

Script Repository

- Find scripts for common tasks here. You can just use these scripts straight + Find scripts for common tasks here. You can just use these scripts straight away or as a reference for building your own more advanced scripts. If you wrote a really useful script please share it with us.