diff --git a/source/net/sourceforge/filebot/format/MediaBindingBean.java b/source/net/sourceforge/filebot/format/MediaBindingBean.java index acffbae3..01b9abd0 100644 --- a/source/net/sourceforge/filebot/format/MediaBindingBean.java +++ b/source/net/sourceforge/filebot/format/MediaBindingBean.java @@ -1,18 +1,26 @@ - package net.sourceforge.filebot.format; - -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static net.sourceforge.filebot.MediaTypes.*; -import static net.sourceforge.filebot.Settings.*; -import static net.sourceforge.filebot.format.Define.*; -import static net.sourceforge.filebot.hash.VerificationUtilities.*; -import static net.sourceforge.filebot.media.MediaDetection.*; -import static net.sourceforge.filebot.similarity.Normalization.*; -import static net.sourceforge.filebot.web.EpisodeFormat.*; -import static net.sourceforge.tuned.FileUtilities.*; -import static net.sourceforge.tuned.StringUtilities.*; +import static java.util.Arrays.asList; +import static java.util.Arrays.sort; +import static java.util.Collections.singleton; +import static net.sourceforge.filebot.MediaTypes.SUBTITLE_FILES; +import static net.sourceforge.filebot.MediaTypes.VIDEO_FILES; +import static net.sourceforge.filebot.Settings.useExtendedFileAttributes; +import static net.sourceforge.filebot.format.Define.undefined; +import static net.sourceforge.filebot.hash.VerificationUtilities.computeHash; +import static net.sourceforge.filebot.hash.VerificationUtilities.getEmbeddedChecksum; +import static net.sourceforge.filebot.hash.VerificationUtilities.getHashFromVerificationFile; +import static net.sourceforge.filebot.media.MediaDetection.releaseInfo; +import static net.sourceforge.filebot.media.MediaDetection.stripReleaseInfo; +import static net.sourceforge.filebot.similarity.Normalization.removeTrailingBrackets; +import static net.sourceforge.filebot.web.EpisodeFormat.SeasonEpisode; +import static net.sourceforge.tuned.FileUtilities.filter; +import static net.sourceforge.tuned.FileUtilities.hasExtension; +import static net.sourceforge.tuned.FileUtilities.isDerived; +import static net.sourceforge.tuned.FileUtilities.listFiles; +import static net.sourceforge.tuned.FileUtilities.readFile; +import static net.sourceforge.tuned.FileUtilities.replacePathSeparators; +import static net.sourceforge.tuned.StringUtilities.join; import java.io.File; import java.io.IOException; @@ -50,31 +58,27 @@ import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter; import com.cedarsoftware.util.io.JsonWriter; - public class MediaBindingBean { - + private final Object infoObject; private final File mediaFile; private final Map context; - + private MediaInfo mediaInfo; private Object metaInfo; - - + public MediaBindingBean(Object infoObject, File mediaFile, Map context) { this.infoObject = infoObject; this.mediaFile = mediaFile; this.context = context; } - - + @Define(undefined) public T undefined() { // omit expressions that depend on undefined values throw new RuntimeException("undefined"); } - - + @Define("n") public String getName() { if (infoObject instanceof Episode) @@ -83,11 +87,10 @@ public class MediaBindingBean { return getMovie().getName(); if (infoObject instanceof AudioTrack) return getAlbumArtist() != null ? getAlbumArtist() : getArtist(); - + return null; } - - + @Define("y") public Integer getYear() { if (infoObject instanceof Episode) @@ -96,23 +99,20 @@ public class MediaBindingBean { return getMovie().getYear(); if (infoObject instanceof AudioTrack) return getReleaseDate() != null ? ((Date) getReleaseDate()).getYear() : new Scanner(getMediaInfo(StreamKind.General, 0, "Recorded_Date")).useDelimiter("\\D+").nextInt(); - + return null; } - - + @Define("s") public Integer getSeasonNumber() { return getEpisode().getSeason(); } - - + @Define("e") public Integer getEpisodeNumber() { return getEpisode().getEpisode(); } - - + @Define("es") public List getEpisodeNumbers() { List n = new ArrayList(); @@ -121,31 +121,28 @@ public class MediaBindingBean { } return n; } - - + @Define("sxe") public String getSxE() { return SeasonEpisode.formatSxE(getEpisode()); } - - + @Define("s00e00") public String getS00E00() { return SeasonEpisode.formatS00E00(getEpisode()); } - - + @Define("t") public String getTitle() { if (infoObject instanceof AudioTrack) { return getMusic().getTrackTitle() != null ? getMusic().getTrackTitle() : getMusic().getTitle(); } - + // single episode format if (getEpisodes().size() == 1) { return getEpisode().getTitle(); } - + // multi-episode format Set title = new LinkedHashSet(); for (Episode it : getEpisodes()) { @@ -153,8 +150,7 @@ public class MediaBindingBean { } return join(title, " & "); } - - + @Define("d") public Object getReleaseDate() { if (infoObject instanceof Episode) { @@ -166,125 +162,132 @@ public class MediaBindingBean { if (infoObject instanceof AudioTrack) { return getMusic().getAlbumReleaseDate(); } - + // no date info for the model return null; } - - + @Define("airdate") public Date airdate() { return getEpisode().getAirdate(); } - - + @Define("startdate") public Date startdate() { return getEpisode().getSeriesStartDate(); } - - + @Define("absolute") public Integer getAbsoluteEpisodeNumber() { return getEpisode().getAbsolute(); } - - + @Define("special") public Integer getSpecialNumber() { return getEpisode().getSpecial(); } - - + @Define("series") public SearchResult getSeriesObject() { return getEpisode().getSeries(); } - - + + @Define("alias") + public List getAliasNames() { + if (infoObject instanceof Movie) { + return asList(getMovie().getAliasNames()); + } + + if (infoObject instanceof Episode) { + return asList(getSeriesObject().getAliasNames()); + } + + return null; + } + @Define("primaryTitle") public String getPrimaryTitle() throws Exception { - if (getSeriesObject() instanceof TheTVDBSearchResult) { - return WebServices.TheTVDB.getSeriesInfo((TheTVDBSearchResult) getSeriesObject(), Locale.ENGLISH).getName(); + if (infoObject instanceof Movie) { + return WebServices.TMDb.getMovieInfo(getMovie(), Locale.ENGLISH).getName(); } - if (getSeriesObject() instanceof AnidbSearchResult) { - return ((AnidbSearchResult) getSeriesObject()).getPrimaryTitle(); + + if (infoObject instanceof Episode) { + if (getSeriesObject() instanceof TheTVDBSearchResult) { + return WebServices.TheTVDB.getSeriesInfo((TheTVDBSearchResult) getSeriesObject(), Locale.ENGLISH).getName(); + } + if (getSeriesObject() instanceof AnidbSearchResult) { + return ((AnidbSearchResult) getSeriesObject()).getPrimaryTitle(); + } + return getSeriesObject().getName(); // default to original search result } - - // default to original search result - return getSeriesObject().getName(); + + return null; } - - + @Define("tmdbid") public String getTmdbId() throws Exception { int tmdbid = getMovie().getTmdbId(); - + if (tmdbid <= 0) { if (getMovie().getImdbId() <= 0) { return null; } - + // lookup IMDbID for TMDbID tmdbid = WebServices.TMDb.getMovieInfo(getMovie(), null).getId(); } - + return String.valueOf(tmdbid); } - - + @Define("imdbid") public String getImdbId() throws Exception { int imdbid = getMovie().getImdbId(); - + if (imdbid <= 0) { if (getMovie().getTmdbId() <= 0) { return null; } - + // lookup IMDbID for TMDbID imdbid = WebServices.TMDb.getMovieInfo(getMovie(), null).getImdbId(); } - + return String.format("tt%07d", imdbid); } - - + @Define("vc") public String getVideoCodec() { // e.g. XviD, x264, DivX 5, MPEG-4 Visual, AVC, etc. String codec = getMediaInfo(StreamKind.Video, 0, "Encoded_Library/Name", "CodecID/Hint", "Format"); - + // get first token (e.g. DivX 5 => DivX) return new Scanner(codec).next(); } - - + @Define("ac") public String getAudioCodec() { // e.g. AC-3, DTS, AAC, Vorbis, MP3, etc. String codec = getMediaInfo(StreamKind.Audio, 0, "CodecID/Hint", "Format"); - + // remove punctuation (e.g. AC-3 => AC3) return codec.replaceAll("\\p{Punct}", ""); } - - + @Define("cf") public String getContainerFormat() { // container format extensions (e.g. avi, mkv mka mks, OGG, etc.) String extensions = getMediaInfo(StreamKind.General, 0, "Codec/Extensions", "Format"); - + // get first extension return new Scanner(extensions).next().toLowerCase(); } - - + @Define("vf") public String getVideoFormat() { int width = Integer.parseInt(getMediaInfo(StreamKind.Video, 0, "Width")); int height = Integer.parseInt(getMediaInfo(StreamKind.Video, 0, "Height")); - + int ns = 0; int[] ws = new int[] { 1920, 1280, 720, 360, 240, 120 }; int[] hs = new int[] { 1080, 720, 480, 360, 240, 120 }; @@ -300,160 +303,146 @@ public class MediaBindingBean { } return null; // video too small } - - + @Define("hpi") public String getExactVideoFormat() { String height = getMediaInfo(StreamKind.Video, 0, "Height"); String scanType = getMediaInfo(StreamKind.Video, 0, "ScanType"); - + if (height == null || scanType == null) return null; - + // e.g. 720p return height + Character.toLowerCase(scanType.charAt(0)); } - - + @Define("af") public String getAudioChannels() { String channels = getMediaInfo(StreamKind.Audio, 0, "Channel(s)"); - + if (channels == null) return null; - + // e.g. 6ch return channels + "ch"; } - - + @Define("resolution") public String getVideoResolution() { List dim = getDimension(); - + if (dim.contains(null)) return null; - + // e.g. 1280x720 return join(dim, "x"); } - - + @Define("ws") public String getWidescreen() { List dim = getDimension(); - + // width-to-height aspect ratio greater than 1.37:1 return (float) dim.get(0) / dim.get(1) > 1.37f ? "ws" : null; } - - + @Define("sdhd") public String getVideoDefinitionCategory() { List dim = getDimension(); - + // SD (less than 720 lines) or HD (more than 720 lines) return dim.get(0) >= 1280 || dim.get(1) >= 720 ? "HD" : "SD"; } - - + @Define("dim") public List getDimension() { String width = getMediaInfo(StreamKind.Video, 0, "Width"); String height = getMediaInfo(StreamKind.Video, 0, "Height"); - + return asList(width != null ? Integer.parseInt(width) : null, height != null ? Integer.parseInt(height) : null); } - - + @Define("original") public String getOriginalFileName() { return getOriginalFileName(mediaFile); } - - + @Define("xattr") public Object getMetaAttributesObject() { return getMetaAttributesObject(mediaFile); } - - + @Define("crc32") public String getCRC32() throws IOException, InterruptedException { // use inferred media file File inferredMediaFile = getInferredMediaFile(); - + // try to get checksum from file name String checksum = getEmbeddedChecksum(inferredMediaFile.getName()); - + if (checksum != null) return checksum; - + // try to get checksum from sfv file checksum = getHashFromVerificationFile(inferredMediaFile, HashType.SFV, 3); - + if (checksum != null) return checksum; - + // calculate checksum from file return crc32(inferredMediaFile); } - - + @Define("fn") public String getFileName() { // make sure media file is defined checkMediaFile(); - + // file extension return FileUtilities.getName(mediaFile); } - - + @Define("ext") public String getExtension() { // make sure media file is defined checkMediaFile(); - + // file extension return FileUtilities.getExtension(mediaFile); } - - + @Define("source") public String getVideoSource() { // use inferred media file File inferredMediaFile = getInferredMediaFile(); - + // look for video source patterns in media file and it's parent folder return releaseInfo.getVideoSource(inferredMediaFile.getParent(), inferredMediaFile.getName(), getOriginalFileName(inferredMediaFile)); } - - + @Define("group") public String getReleaseGroup() throws IOException { // use inferred media file File inferredMediaFile = getInferredMediaFile(); - + // look for release group names in media file and it's parent folder return releaseInfo.getReleaseGroup(inferredMediaFile.getParent(), inferredMediaFile.getName(), getOriginalFileName(inferredMediaFile)); } - - + @Define("lang") public Locale detectSubtitleLanguage() throws Exception { // make sure media file is defined checkMediaFile(); - + Locale languageSuffix = releaseInfo.getLanguageSuffix(FileUtilities.getName(mediaFile)); if (languageSuffix != null) return new Locale(languageSuffix.getISO3Language()); // force ISO3 letter-code - + // require subtitle file if (!SUBTITLE_FILES.accept(mediaFile)) { return null; } - + // exclude VobSub from any normal text-based subtitle processing if (hasExtension(mediaFile, "idx")) { return null; @@ -464,51 +453,44 @@ public class MediaBindingBean { } } } - + // try statistical language detection return WebServices.OpenSubtitles.detectLanguage(readFile(mediaFile)); } - - + @Define("actors") public Object getActors() { return getMetaInfo().getProperty("actors"); } - - + @Define("genres") public Object getGenres() { if (infoObject instanceof AudioTrack) return asList(getMediaInfo(StreamKind.General, 0, "Genre").split(";")); - + return getMetaInfo().getProperty("genres"); } - - + @Define("director") public Object getDirector() { return getMetaInfo().getProperty("director"); } - - + @Define("certification") public Object getCertification() { return getMetaInfo().getProperty("certification"); } - - + @Define("rating") public Object getRating() { return getMetaInfo().getProperty("rating"); } - - + @Define("collection") public Object getCollection() { return getMetaInfo().getProperty("collection"); } - - + @Define("info") public synchronized AssociativeScriptObject getMetaInfo() { if (metaInfo == null) { @@ -521,15 +503,14 @@ public class MediaBindingBean { throw new RuntimeException("Failed to retrieve metadata: " + infoObject, e); } } - + return createMapBindings(new PropertyBindings(metaInfo, null)); } - - + @Define("imdb") public synchronized AssociativeScriptObject getImdbApiInfo() { Object data = null; - + try { if (infoObject instanceof Episode) { data = WebServices.IMDb.getImdbApiMovieInfo(new Movie(getEpisode().getSeriesName(), getEpisode().getSeriesStartDate().getYear(), -1, -1)); @@ -541,153 +522,130 @@ public class MediaBindingBean { } catch (Exception e) { throw new RuntimeException("Failed to retrieve metadata: " + infoObject, e); } - + return createMapBindings(new PropertyBindings(data, null)); } - - + @Define("episodelist") public Object getEpisodeList() throws Exception { return WebServices.TheTVDB.getEpisodeList(WebServices.TheTVDB.search(getEpisode().getSeriesName()).get(0), SortOrder.Airdate, Locale.ENGLISH); } - - + @Define("media") public AssociativeScriptObject getGeneralMediaInfo() { return createMapBindings(getMediaInfo().snapshot(StreamKind.General, 0)); } - - + @Define("video") public AssociativeScriptObject getVideoInfo() { return createMapBindings(getMediaInfo().snapshot(StreamKind.Video, 0)); } - - + @Define("audio") public AssociativeScriptObject getAudioInfo() { return createMapBindings(getMediaInfo().snapshot(StreamKind.Audio, 0)); } - - + @Define("text") public AssociativeScriptObject getTextInfo() { return createMapBindings(getMediaInfo().snapshot(StreamKind.Text, 0)); } - - + @Define("videos") public List getVideoInfoList() { return createMapBindingsList(getMediaInfo().snapshot().get(StreamKind.Video)); } - - + @Define("audios") public List getAudioInfoList() { return createMapBindingsList(getMediaInfo().snapshot().get(StreamKind.Audio)); } - - + @Define("texts") public List getTextInfoList() { return createMapBindingsList(getMediaInfo().snapshot().get(StreamKind.Text)); } - - + @Define("artist") public String getArtist() { return getMusic().getArtist(); } - - + @Define("albumArtist") public String getAlbumArtist() { return getMusic().getAlbumArtist(); } - - + @Define("album") public String getAlbum() { return getMusic().getAlbum(); } - - + @Define("episode") public Episode getEpisode() { return (Episode) infoObject; } - - + @Define("episodes") public List getEpisodes() { return infoObject instanceof MultiEpisode ? ((MultiEpisode) infoObject).getEpisodes() : asList(getEpisode()); } - - + @Define("movie") public Movie getMovie() { return (Movie) infoObject; } - - + @Define("music") public AudioTrack getMusic() { return (AudioTrack) infoObject; } - - + @Define("pi") public Integer getPart() { if (infoObject instanceof AudioTrack) return getMusic().getTrack() != null ? getMusic().getTrack() : Integer.parseInt(getMediaInfo(StreamKind.General, 0, "Track/Position")); if (infoObject instanceof MoviePart) return ((MoviePart) infoObject).getPartIndex(); - + return null; } - - + @Define("pn") public Integer getPartCount() { if (infoObject instanceof AudioTrack) return getMusic().getTrackCount(); if (infoObject instanceof MoviePart) return ((MoviePart) infoObject).getPartCount(); - + return null; } - - + @Define("file") public File getMediaFile() { return mediaFile; } - - + @Define("folder") public File getMediaParentFolder() { return getMediaFile().getParentFile(); } - - + @Define("home") public File getUserHome() throws IOException { return new File(System.getProperty("user.home")); } - - + @Define("object") public Object getInfoObject() { return infoObject; } - - + @Define("i") public Integer getModelIndex() { return identityIndexOf(getContext().values(), getInfoObject()); } - - + @Define("di") public Integer getDuplicateIndex() { List duplicates = new ArrayList(); @@ -699,24 +657,21 @@ public class MediaBindingBean { int di = identityIndexOf(duplicates, getInfoObject()); return di == 0 ? null : di; } - - + @Define("model") public Map getContext() { return context; } - - + @Define("json") public String getInfoObjectDump() throws Exception { return JsonWriter.objectToJson(infoObject); } - - + private File getInferredMediaFile() { // make sure media file is defined checkMediaFile(); - + if (mediaFile.isDirectory()) { // just select the first video file in the folder as media sample SortedSet videos = new TreeSet(filter(listFiles(singleton(mediaFile), 2, false), VIDEO_FILES)); @@ -732,22 +687,22 @@ public class MediaBindingBean { } } } - + // file is a subtitle, or nfo, etc String baseName = stripReleaseInfo(FileUtilities.getName(mediaFile)).toLowerCase(); File[] videos = mediaFile.getParentFile().listFiles(VIDEO_FILES); - + // find corresponding movie file for (File movieFile : videos) { if (!baseName.isEmpty() && stripReleaseInfo(FileUtilities.getName(movieFile)).toLowerCase().startsWith(baseName)) { return movieFile; } } - + // still no good match found -> just take the most probable video from the same folder if (videos.length > 0) { sort(videos, new SimilarityComparator(FileUtilities.getName(mediaFile)) { - + @Override public int compare(Object o1, Object o2) { return super.compare(FileUtilities.getName((File) o1), FileUtilities.getName((File) o2)); @@ -756,37 +711,34 @@ public class MediaBindingBean { return videos[0]; } } - + return mediaFile; } - - + private void checkMediaFile() throws RuntimeException { // make sure file is not null, and that it is an existing file if (mediaFile == null) throw new RuntimeException("Invalid media file: " + mediaFile); } - - + private synchronized MediaInfo getMediaInfo() { if (mediaInfo == null) { // make sure media file is defined checkMediaFile(); - + MediaInfo newMediaInfo = new MediaInfo(); - + // use inferred media file (e.g. actual movie file instead of subtitle file) if (!newMediaInfo.open(getInferredMediaFile())) { throw new RuntimeException("Cannot open media file: " + mediaFile); } - + mediaInfo = newMediaInfo; } - + return mediaInfo; } - - + private Integer identityIndexOf(Iterable c, Object o) { Iterator itr = c.iterator(); for (int i = 0; itr.hasNext(); i++) { @@ -796,42 +748,39 @@ public class MediaBindingBean { } return null; } - - + private String getMediaInfo(StreamKind streamKind, int streamNumber, String... keys) { for (String key : keys) { String value = getMediaInfo().get(streamKind, streamNumber, key); - + if (value.length() > 0) return value; } - + return null; } - - + private AssociativeScriptObject createMapBindings(Map map) { return new AssociativeScriptObject(map) { - + @Override public Object getProperty(String name) { Object value = super.getProperty(name); - + if (value == null) { throw new BindingException(name, "undefined"); } - + // auto-clean value of path separators if (value instanceof CharSequence) { return replacePathSeparators(value.toString()).trim(); } - + return value; } }; } - - + private List createMapBindingsList(List> mapList) { List bindings = new ArrayList(); for (Map it : mapList) { @@ -839,24 +788,22 @@ public class MediaBindingBean { } return bindings; } - - + private String crc32(File file) throws IOException, InterruptedException { // try to get checksum from cache Cache cache = Cache.getCache("checksum"); - + String hash = cache.get(file, String.class); if (hash != null) { return hash; } - + // compute and cache checksum hash = computeHash(file, HashType.SFV); cache.put(file, hash); return hash; } - - + private String getOriginalFileName(File file) { if (useExtendedFileAttributes()) { try { @@ -867,8 +814,7 @@ public class MediaBindingBean { } return null; } - - + private Object getMetaAttributesObject(File file) { if (useExtendedFileAttributes()) { try { @@ -879,5 +825,5 @@ public class MediaBindingBean { } return null; } - + } diff --git a/source/net/sourceforge/filebot/media/MediaDetection.java b/source/net/sourceforge/filebot/media/MediaDetection.java index 79d0eec1..aa376e51 100644 --- a/source/net/sourceforge/filebot/media/MediaDetection.java +++ b/source/net/sourceforge/filebot/media/MediaDetection.java @@ -611,7 +611,7 @@ public class MediaDetection { sort(sorted, new SimilarityComparator(getMovieMatchMetric(), paragon.toArray())); // DEBUG - // System.out.format("sortBySimilarity %s => %s", terms, options); + // System.out.format("sortBySimilarity %s => %s", terms, sorted); return sorted; } diff --git a/source/net/sourceforge/filebot/media/ReleaseInfo.properties b/source/net/sourceforge/filebot/media/ReleaseInfo.properties index baf9a79c..89a73ac5 100644 --- a/source/net/sourceforge/filebot/media/ReleaseInfo.properties +++ b/source/net/sourceforge/filebot/media/ReleaseInfo.properties @@ -5,25 +5,25 @@ pattern.video.source: CAMRip|CAM|PDVD|TS|TELESYNC|PDVD|PPV|PPVRip|Screener|SCR|S pattern.video.format: DivX|Xvid|AVC|x264|h264|3ivx|mpg|mpeg|mpeg4|mp3|AAC|AAC2.0|AAC5.1|AAC.2.0|AAC.5.1|AC3|dd20|dd51|2ch|6ch|TS|DTS|DTS.HD|DTS.HD.MA|TrueHD|WS|HR|7p|720p|18p|1080p|PAL|NTSC|3D # known release group names -url.release-groups: http://filebot.net/data/release-groups.txt +url.release-groups: file:///d:/workspace/filebot/website/data/release-groups.txt # blacklisted terms that will be ignored -url.query-blacklist: http://filebot.net/data/query-blacklist.txt +url.query-blacklist: file:///d:/workspace/filebot/website/data/query-blacklist.txt # clutter files that will be ignored -url.exclude-blacklist: http://filebot.net/data/exclude-blacklist.txt +url.exclude-blacklist: file:///d:/workspace/filebot/website/data/exclude-blacklist.txt # list of patterns directly matching files to series names -url.series-mappings: http://filebot.net/data/series-mappings.txt +url.series-mappings: file:///d:/workspace/filebot/website/data/series-mappings.txt # list of all movies (id, name, year) -url.movie-list: http://filebot.net/data/movies.txt.xz +url.movie-list: file:///d:/workspace/filebot/website/data/movies.txt.xz # TheTVDB index -url.thetvdb-index: http://filebot.net/data/thetvdb.txt.xz +url.thetvdb-index: file:///d:/workspace/filebot/website/data/thetvdb.txt.xz # AniDB index -url.anidb-index: http://filebot.net/data/anidb.txt.xz +url.anidb-index: file:///d:/workspace/filebot/website/data/anidb.txt.xz # disk folder matcher pattern.diskfolder.entry: BDMV|HVDVD_TS|VIDEO_TS|AUDIO_TS|VCD|movie.nfo diff --git a/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java b/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java index 9f8446a9..39575ece 100644 --- a/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java +++ b/source/net/sourceforge/filebot/similarity/EpisodeMetrics.java @@ -1,20 +1,31 @@ - package net.sourceforge.filebot.similarity; - -import static java.lang.Math.*; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static net.sourceforge.filebot.Settings.*; -import static net.sourceforge.filebot.similarity.Normalization.*; -import static net.sourceforge.tuned.FileUtilities.*; -import static net.sourceforge.tuned.StringUtilities.*; +import static java.lang.Math.ceil; +import static java.lang.Math.floor; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.synchronizedMap; +import static net.sourceforge.filebot.Settings.useExtendedFileAttributes; +import static net.sourceforge.filebot.similarity.Normalization.normalizePunctuation; +import static net.sourceforge.filebot.similarity.Normalization.removeEmbeddedChecksum; +import static net.sourceforge.filebot.similarity.Normalization.removeTrailingBrackets; +import static net.sourceforge.tuned.FileUtilities.getName; +import static net.sourceforge.tuned.FileUtilities.getNameWithoutExtension; +import static net.sourceforge.tuned.FileUtilities.getRelativePathTail; +import static net.sourceforge.tuned.FileUtilities.normalizePathSeparators; +import static net.sourceforge.tuned.StringUtilities.join; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -36,125 +47,121 @@ import net.sourceforge.filebot.web.TheTVDBSearchResult; import com.ibm.icu.text.Transliterator; - public enum EpisodeMetrics implements SimilarityMetric { - + // Match by season / episode numbers SeasonEpisode(new SeasonEpisodeMetric() { - + private final Map> transformCache = synchronizedMap(new HashMap>(64, 4)); - - + @Override protected Collection parse(Object object) { if (object instanceof Movie) { return emptySet(); } - + Collection result = transformCache.get(object); if (result != null) { return result; } - + if (object instanceof Episode) { Episode episode = (Episode) object; - + if (episode.getSpecial() != null) { return singleton(new SxE(0, episode.getSpecial())); } - + // get SxE from episode, both SxE for season/episode numbering and SxE for absolute episode numbering SxE sxe = new SxE(episode.getSeason(), episode.getEpisode()); SxE abs = new SxE(null, episode.getAbsolute()); - + result = (abs.episode < 0 || sxe.equals(abs)) ? singleton(sxe) : asList(sxe, abs); } else { result = super.parse(object); } - + transformCache.put(object, result); return result; } }), - + // Match episode airdate AirDate(new DateMetric() { - + private final Map transformCache = synchronizedMap(new HashMap(64, 4)); - - + @Override public Date parse(Object object) { if (object instanceof Movie) { return null; } - + if (object instanceof Episode) { Episode episode = (Episode) object; - + // use airdate from episode return episode.getAirdate(); } - + Date result = transformCache.get(object); if (result != null) { return result; } - + result = super.parse(object); transformCache.put(object, result); return result; } }), - + // Match by episode/movie title Title(new SubstringMetric() { - + @Override protected String normalize(Object object) { if (object instanceof Episode) { Episode e = (Episode) object; - + // don't use title for matching if title equals series name String normalizedToken = normalizeObject(e.getTitle()); if (normalizedToken.length() >= 3 && !normalizeObject(e.getSeriesName()).contains(normalizedToken)) { return normalizedToken; } } - + if (object instanceof Movie) { object = ((Movie) object).getName(); } - + return normalizeObject(object); } }), - + // Match by SxE and airdate EpisodeIdentifier(new MetricCascade(SeasonEpisode, AirDate)), - - // Advanced episode <-> file matching Lv1 + + // Advanced episode <-> file matching Lv1 EpisodeFunnel(new MetricCascade(SeasonEpisode, AirDate, Title)), - + // Advanced episode <-> file matching Lv2 EpisodeBalancer(new SimilarityMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { float sxe = EpisodeIdentifier.getSimilarity(o1, o2); float title = Title.getSimilarity(o1, o2); - + // account for misleading SxE patterns in the episode title if (sxe < 0 && title == 1 && EpisodeIdentifier.getSimilarity(getTitle(o1), getTitle(o2)) == 1) { sxe = 1; title = 0; } - + // 1:SxE && Title, 2:SxE return (float) ((max(sxe, 0) * title) + (floor(sxe) / 10)); } - - + public Object getTitle(Object o) { if (o instanceof Episode) { Episode e = (Episode) o; @@ -163,15 +170,15 @@ public enum EpisodeMetrics implements SimilarityMetric { return o; } }), - + // Match series title and episode title against folder structure and file name SubstringFields(new SubstringMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { String[] f1 = normalize(fields(o1)); String[] f2 = normalize(fields(o2)); - + // match all fields and average similarity float sum = 0; for (String s1 : f1) { @@ -180,60 +187,64 @@ public enum EpisodeMetrics implements SimilarityMetric { } } sum /= f1.length * f2.length; - + // normalize into 3 similarity levels return (float) (ceil(sum * 3) / 3); } - - + protected String[] normalize(Object[] objects) { String[] names = new String[objects.length]; - + for (int i = 0; i < objects.length; i++) { names[i] = normalizeObject(objects[i]).replaceAll("\\s", ""); } - + return names; } - - + protected Object[] fields(Object object) { if (object instanceof Episode) { Episode episode = (Episode) object; - String seriesName = removeTrailingBrackets(episode.getSeriesName()); - String episodeTitle = episode.getTitle(); - if (!seriesName.equalsIgnoreCase(episodeTitle)) { - return new Object[] { seriesName, episodeTitle }; - } else { - return new Object[] { seriesName, null }; + LinkedHashSet set = new LinkedHashSet(4); + set.add(removeTrailingBrackets(episode.getSeriesName())); + set.add(removeTrailingBrackets(episode.getTitle())); + set.add(removeTrailingBrackets(episode.getSeries().getName())); + for (String it : episode.getSeries().getAliasNames()) { + set.add(removeTrailingBrackets(it)); } + + Iterator itr = set.iterator(); + Object[] f = new Object[4]; + for (int i = 0; i < f.length; i++) { + f[i] = itr.hasNext() ? itr.next() : null; + } + return f; } - + if (object instanceof File) { File file = (File) object; return new Object[] { file.getParentFile().getAbsolutePath(), file }; } - + if (object instanceof Movie) { Movie movie = (Movie) object; return new Object[] { movie.getName(), movie.getYear() }; } - + return new Object[] { object }; } }), - + // Match via common word sequence in episode name and file name NameSubstringSequence(new SequenceMatchSimilarity() { - + @Override public float getSimilarity(Object o1, Object o2) { // normalize absolute similarity to similarity rank (4 ranks in total), // so we are less likely to fall for false positives in this pass, and move on to the next one return (float) (floor(super.getSimilarity(o1, o2) * 4) / 4); } - - + @Override protected String normalize(Object object) { if (object instanceof Episode) { @@ -247,47 +258,43 @@ public enum EpisodeMetrics implements SimilarityMetric { return normalizeObject(object); } }), - + // Match by generic name similarity (round rank) Name(new NameSimilarityMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { // normalize absolute similarity to similarity rank (4 ranks in total), // so we are less likely to fall for false positives in this pass, and move on to the next one return (float) (floor(super.getSimilarity(o1, o2) * 4) / 4); } - - + @Override protected String normalize(Object object) { // simplify file name, if possible return normalizeObject(object); } }), - + // Match by generic name similarity (absolute) SeriesName(new NameSimilarityMetric() { - + private ReleaseInfo releaseInfo = new ReleaseInfo(); private SeriesNameMatcher seriesNameMatcher = new SeriesNameMatcher(); - - + @Override public float getSimilarity(Object o1, Object o2) { float lowerBound = super.getSimilarity(normalize(o1, true), normalize(o2, true)); float upperBound = super.getSimilarity(normalize(o1, false), normalize(o2, false)); - + return (float) (floor(max(lowerBound, upperBound) * 4) / 4); }; - - + @Override protected String normalize(Object object) { return object.toString(); }; - - + protected String normalize(Object object, boolean strict) { if (object instanceof Episode) { if (strict) { @@ -302,7 +309,7 @@ public enum EpisodeMetrics implements SimilarityMetric { object = sn; } } - + // equally strip away strip potential any clutter try { object = releaseInfo.cleanRelease(singleton(object.toString()), strict).iterator().next(); @@ -311,15 +318,15 @@ public enum EpisodeMetrics implements SimilarityMetric { } catch (IOException e) { Logger.getLogger(EpisodeMetrics.class.getName()).log(Level.WARNING, e.getMessage()); } - + // simplify file name, if possible return normalizeObject(object); } }), - + // Match by generic name similarity (absolute) AbsolutePath(new NameSimilarityMetric() { - + @Override protected String normalize(Object object) { if (object instanceof File) { @@ -328,24 +335,22 @@ public enum EpisodeMetrics implements SimilarityMetric { return normalizeObject(object.toString()); // simplify file name, if possible } }), - + NumericSequence(new SequenceMatchSimilarity() { - + @Override public float getSimilarity(Object o1, Object o2) { float lowerBound = super.getSimilarity(normalize(o1, true), normalize(o2, true)); float upperBound = super.getSimilarity(normalize(o1, false), normalize(o2, false)); - + return max(lowerBound, upperBound); }; - - + @Override protected String normalize(Object object) { return object.toString(); }; - - + protected String normalize(Object object, boolean numbersOnly) { if (object instanceof Episode) { Episode e = (Episode) object; @@ -362,7 +367,7 @@ public enum EpisodeMetrics implements SimilarityMetric { object = String.format("%s %s", m.getName(), m.getYear()); } } - + // simplify file name if possible and extract numbers List numbers = new ArrayList(4); Scanner scanner = new Scanner(normalizeObject(object)).useDelimiter("\\D+"); @@ -372,15 +377,15 @@ public enum EpisodeMetrics implements SimilarityMetric { return join(numbers, " "); } }), - + // Match by generic numeric similarity Numeric(new NumericSimilarityMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { String[] f1 = fields(o1); String[] f2 = fields(o2); - + // match all fields and average similarity float max = 0; for (String s1 : f1) { @@ -390,106 +395,102 @@ public enum EpisodeMetrics implements SimilarityMetric { } return max; } - - + protected String[] fields(Object object) { if (object instanceof Episode) { Episode episode = (Episode) object; return new String[] { episode.getSeriesName(), EpisodeFormat.SeasonEpisode.formatSxE(episode), String.valueOf(episode.getAbsolute()) }; } - + if (object instanceof Movie) { Movie movie = (Movie) object; return new String[] { movie.getName(), String.valueOf(movie.getYear()) }; } - + return new String[] { normalizeObject(object) }; } }), - + // Match by file length (only works when matching torrents or files) FileSize(new FileSizeMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { // order of arguments is logically irrelevant, but we might be able to save us a call to File.length() which is quite costly return o1 instanceof File ? super.getSimilarity(o2, o1) : super.getSimilarity(o1, o2); } - - + @Override protected long getLength(Object object) { if (object instanceof FileInfo) { return ((FileInfo) object).getLength(); } - + return super.getLength(object); } }), - + // Match by common words at the beginning of both files FileName(new FileNameMetric() { - + @Override protected String getFileName(Object object) { if (object instanceof File || object instanceof FileInfo) { return normalizeObject(object); } - + return null; } }), - + // Match by file last modified and episode release dates TimeStamp(new TimeStampMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { // adjust differentiation accuracy to about a year float f = super.getSimilarity(o1, o2); return f >= 0.8 ? 1 : f >= 0 ? 0 : -1; } - - + @Override public long getTimeStamp(Object object) { if (object instanceof Episode) { try { long ts = ((Episode) object).getAirdate().getTimeStamp(); - + // big penalty for episodes not yet aired if (ts > System.currentTimeMillis()) { return -1; } - + return ts; } catch (RuntimeException e) { return -1; // some episodes may not have airdate defined } } - + return super.getTimeStamp(object); } }), - + SeriesRating(new SimilarityMetric() { - + @Override public float getSimilarity(Object o1, Object o2) { float r1 = getRating(o1); float r2 = getRating(o2); return max(r1, r2) >= 0.4 ? 1 : min(r1, r2) < 0 ? -1 : 0; } - + private final Map seriesInfoCache = new HashMap(); - - + public float getRating(Object o) { if (o instanceof Episode) { try { synchronized (seriesInfoCache) { String n = ((Episode) o).getSeriesName(); - + SeriesInfo seriesInfo = seriesInfoCache.get(n); if (seriesInfo == null && !seriesInfoCache.containsKey(n)) { try { @@ -499,7 +500,7 @@ public enum EpisodeMetrics implements SimilarityMetric { } seriesInfoCache.put(n, seriesInfo); } - + if (seriesInfo != null) { if (seriesInfo.getRatingCount() > 0) { float rating = max(0, seriesInfo.getRating().floatValue()); @@ -516,18 +517,18 @@ public enum EpisodeMetrics implements SimilarityMetric { return 0; } }), - + // Match by stored MetaAttributes if possible MetaAttributes(new CrossPropertyMetric() { - + @Override protected Map getProperties(Object object) { // Episode / Movie objects if (object instanceof Episode || object instanceof Movie) { return super.getProperties(object); } - - // deserialize MetaAttributes if enabled and available + + // deserialize MetaAttributes if enabled and available if (object instanceof File && useExtendedFileAttributes()) { try { return super.getProperties(new net.sourceforge.filebot.media.MetaAttributes((File) object).getObject()); @@ -535,68 +536,64 @@ public enum EpisodeMetrics implements SimilarityMetric { // ignore } } - + // ignore everything else return emptyMap(); }; - + }); - + // inner metric private final SimilarityMetric metric; - - + private EpisodeMetrics(SimilarityMetric metric) { this.metric = metric; } - - + @Override public float getSimilarity(Object o1, Object o2) { return metric.getSimilarity(o1, o2); } - + private static final Map transformCache = synchronizedMap(new HashMap(64, 4)); private static final Transliterator transliterator = Transliterator.getInstance("Any-Latin;Latin-ASCII;[:Diacritic:]remove"); - - + protected static String normalizeObject(Object object) { if (object == null) { return ""; } - + String result = transformCache.get(object); if (result != null) { return result; } - + String name = object.toString(); - + // use name without extension if (object instanceof File) { name = getName((File) object); } else if (object instanceof FileInfo) { name = ((FileInfo) object).getName(); } - + // remove checksums, any [...] or (...) name = removeEmbeddedChecksum(name); - + synchronized (transliterator) { name = transliterator.transform(name); } - + // remove/normalize special characters name = normalizePunctuation(name); - + // normalize to lower case name = name.toLowerCase(); - + transformCache.put(object, name); return name; } - - + public static SimilarityMetric[] defaultSequence(boolean includeFileMetrics) { // 1 pass: divide by file length (only works for matching torrent entries or files) // 2-3 pass: divide by title or season / episode numbers @@ -611,10 +608,9 @@ public enum EpisodeMetrics implements SimilarityMetric { return new SimilarityMetric[] { EpisodeFunnel, EpisodeBalancer, SubstringFields, MetaAttributes, new MetricCascade(NameSubstringSequence, Name), Numeric, NumericSequence, SeriesName, SeriesRating, TimeStamp, AbsolutePath }; } } - - + public static SimilarityMetric verificationMetric() { return new MetricCascade(FileSize, FileName, SeasonEpisode, AirDate, Title, Name); } - + } diff --git a/source/net/sourceforge/filebot/similarity/SubstringMetric.java b/source/net/sourceforge/filebot/similarity/SubstringMetric.java index 34cf26de..9471235c 100644 --- a/source/net/sourceforge/filebot/similarity/SubstringMetric.java +++ b/source/net/sourceforge/filebot/similarity/SubstringMetric.java @@ -1,35 +1,34 @@ - package net.sourceforge.filebot.similarity; - -import static net.sourceforge.filebot.similarity.Normalization.*; - +import static net.sourceforge.filebot.similarity.Normalization.normalizePunctuation; public class SubstringMetric implements SimilarityMetric { - + @Override public float getSimilarity(Object o1, Object o2) { String s1 = normalize(o1); if (s1 == null || s1.isEmpty()) return 0; - + String s2 = normalize(o2); if (s2 == null || s2.isEmpty()) return 0; - + return s1.contains(s2) || s2.contains(s1) ? 1 : 0; } - - + protected String normalize(Object object) { + if (object == null) + return null; + // use string representation String name = object.toString(); - + // normalize separators name = normalizePunctuation(name); - + // normalize case and trim return name.trim().toLowerCase(); } - + } diff --git a/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties b/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties index c5cf1369..40ee216e 100644 --- a/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties +++ b/source/net/sourceforge/filebot/ui/rename/FormatDialog.properties @@ -4,8 +4,8 @@ episode.syntax: { } \u2026 expression, n \u2026 name, movie.syntax: { } \u2026 expression, n \u2026 name, y \u2026 year music.syntax: { } \u2026 expression, n \u2026 album artist, t \u2026 title, album \u2026 album, pi \u2026 track -episode.sample: {"@type":"net.sourceforge.filebot.web.Episode","seriesName":"Firefly","seriesStartDate":{"year":2002,"month":9,"day":20},"season":1,"episode":1,"title":"Serenity","absolute":1,"special":null,"airdate":{"year":2002,"month":12,"day":20},"series":{"@type":"net.sourceforge.filebot.web.TheTVDBSearchResult","seriesId":78874,"name":"Firefly"}} -movie.sample: {"@type":"net.sourceforge.filebot.web.MoviePart","partIndex":1,"partCount":2,"year":2009,"imdbId":-1,"tmdbId":19995,"name":"Avatar"} +episode.sample: {"@type":"net.sourceforge.filebot.web.Episode","seriesName":"Firefly","seriesStartDate":{"year":2002,"month":9,"day":20},"season":1,"episode":1,"title":"Serenity","absolute":1,"special":null,"airdate":{"year":2002,"month":12,"day":20},"series":{"@type":"net.sourceforge.filebot.web.TheTVDBSearchResult","seriesId":78874,"name":"Firefly","aliasNames":[]}} +movie.sample: {"@type":"net.sourceforge.filebot.web.MoviePart","partIndex":1,"partCount":2,"year":2009,"imdbId":-1,"tmdbId":19995,"name":"Avatar","aliasNames":[]} music.sample: {"@type":"net.sourceforge.filebot.web.AudioTrack","artist":"Leona Lewis","title":"I See You","album":"Avatar","albumArtist":"James Horner","trackTitle":null,"albumReleaseDate":{"year":2009,"month":12,"day":11},"mediumIndex":1,"mediumCount":1,"trackIndex":14,"trackCount":14} # basic 1.01 diff --git a/source/net/sourceforge/filebot/web/AnidbSearchResult.java b/source/net/sourceforge/filebot/web/AnidbSearchResult.java index cf021f3a..f942f4e3 100644 --- a/source/net/sourceforge/filebot/web/AnidbSearchResult.java +++ b/source/net/sourceforge/filebot/web/AnidbSearchResult.java @@ -1,65 +1,51 @@ - package net.sourceforge.filebot.web; - public class AnidbSearchResult extends SearchResult { - + protected int aid; - protected String primaryTitle; // one per anime - protected String englishTitle; // one per language - - + protected AnidbSearchResult() { // used by serializer } - - + public AnidbSearchResult(int aid, String primaryTitle, String englishTitle) { + super(primaryTitle, englishTitle); this.aid = aid; - this.primaryTitle = primaryTitle; - this.englishTitle = englishTitle; } - - + public int getId() { return aid; } - - + public int getAnimeId() { return aid; } - - + @Override public String getName() { - return primaryTitle; + return name; } - - + public String getPrimaryTitle() { - return primaryTitle; + return name; } - - + public String getEnglishTitle() { - return englishTitle; + return aliasNames.length > 0 ? aliasNames[0] : null; } - - + @Override public int hashCode() { return aid; } - - + @Override public boolean equals(Object object) { if (object instanceof AnidbSearchResult) { AnidbSearchResult other = (AnidbSearchResult) object; return this.aid == other.aid; } - + return false; } } \ No newline at end of file diff --git a/source/net/sourceforge/filebot/web/Movie.java b/source/net/sourceforge/filebot/web/Movie.java index 213ab111..7fb1d3f0 100644 --- a/source/net/sourceforge/filebot/web/Movie.java +++ b/source/net/sourceforge/filebot/web/Movie.java @@ -1,50 +1,44 @@ - package net.sourceforge.filebot.web; - import java.util.Arrays; - public class Movie extends SearchResult { - + protected int year; protected int imdbId; protected int tmdbId; - - + protected Movie() { // used by serializer } - - + public Movie(Movie obj) { - this(obj.name, obj.year, obj.imdbId, obj.tmdbId); + this(obj.name, obj.aliasNames, obj.year, obj.imdbId, obj.tmdbId); } - - + public Movie(String name, int year, int imdbId, int tmdbId) { - super(name); + this(name, new String[0], year, imdbId, tmdbId); + } + + public Movie(String name, String[] aliasNames, int year, int imdbId, int tmdbId) { + super(name, aliasNames); this.year = year; this.imdbId = imdbId; this.tmdbId = tmdbId; } - - + public int getYear() { return year; } - - + public int getImdbId() { return imdbId; } - - + public int getTmdbId() { return tmdbId; } - - + @Override public boolean equals(Object object) { if (object instanceof Movie) { @@ -54,29 +48,26 @@ public class Movie extends SearchResult { } else if (tmdbId > 0 && other.tmdbId > 0) { return tmdbId == other.tmdbId; } - - return year == other.year && name.equalsIgnoreCase(other.name); + + return year == other.year && name.equals(other.name); } - + return false; } - - + @Override public Movie clone() { return new Movie(this); } - - + @Override public int hashCode() { return Arrays.hashCode(new Object[] { name.toLowerCase(), year }); } - - + @Override public String toString() { return String.format("%s (%04d)", name, year < 0 ? 0 : year); } - + } diff --git a/source/net/sourceforge/filebot/web/SearchResult.java b/source/net/sourceforge/filebot/web/SearchResult.java index e6f44adb..d97d0340 100644 --- a/source/net/sourceforge/filebot/web/SearchResult.java +++ b/source/net/sourceforge/filebot/web/SearchResult.java @@ -1,33 +1,32 @@ - package net.sourceforge.filebot.web; - import java.io.Serializable; - public abstract class SearchResult implements Serializable { - - protected final String name; - + + protected String name; + protected String[] aliasNames; protected SearchResult() { - this.name = null; + // used by serializer } - - public SearchResult(String name) { + public SearchResult(String name, String... aliasNames) { this.name = name; + this.aliasNames = aliasNames; } - public String getName() { return name; } - + + public String[] getAliasNames() { + return aliasNames.clone(); + } @Override public String toString() { - return getName(); + return name; } - + } diff --git a/source/net/sourceforge/filebot/web/TMDbClient.java b/source/net/sourceforge/filebot/web/TMDbClient.java index 73786a81..b9ea1a5a 100644 --- a/source/net/sourceforge/filebot/web/TMDbClient.java +++ b/source/net/sourceforge/filebot/web/TMDbClient.java @@ -74,8 +74,9 @@ public class TMDbClient implements MovieIdentificationService { // e.g. // {"id":16320,"title":"冲出宁静号","release_date":"2005-09-30","original_title":"Serenity"} String title = (String) it.get("title"); + String originalTitle = (String) it.get("original_title"); if (title == null || title.isEmpty()) { - title = (String) it.get("original_title"); + title = originalTitle; } try { @@ -87,7 +88,7 @@ public class TMDbClient implements MovieIdentificationService { } catch (Exception e) { throw new IllegalArgumentException("Missing data: year"); } - result.add(new Movie(title, year, -1, (int) id)); + result.add(new Movie(title, title.equals(originalTitle) ? new String[] {} : new String[] { originalTitle }, year, -1, (int) id)); } catch (Exception e) { Logger.getLogger(TMDbClient.class.getName()).log(Level.FINE, String.format("Ignore movie [%s]: %s", title, e.getMessage())); } diff --git a/source/net/sourceforge/filebot/web/TheTVDBClient.java b/source/net/sourceforge/filebot/web/TheTVDBClient.java index a3aa545c..f1c02869 100644 --- a/source/net/sourceforge/filebot/web/TheTVDBClient.java +++ b/source/net/sourceforge/filebot/web/TheTVDBClient.java @@ -1,11 +1,15 @@ - package net.sourceforge.filebot.web; - -import static java.util.Arrays.*; -import static net.sourceforge.filebot.web.EpisodeUtilities.*; -import static net.sourceforge.filebot.web.WebRequest.*; -import static net.sourceforge.tuned.XPathUtilities.*; +import static java.util.Arrays.asList; +import static net.sourceforge.filebot.web.EpisodeUtilities.filterBySeason; +import static net.sourceforge.filebot.web.EpisodeUtilities.sortEpisodes; +import static net.sourceforge.filebot.web.WebRequest.encode; +import static net.sourceforge.filebot.web.WebRequest.getDocument; +import static net.sourceforge.tuned.XPathUtilities.getIntegerContent; +import static net.sourceforge.tuned.XPathUtilities.getTextContent; +import static net.sourceforge.tuned.XPathUtilities.selectNode; +import static net.sourceforge.tuned.XPathUtilities.selectNodes; +import static net.sourceforge.tuned.XPathUtilities.selectString; import java.io.FileNotFoundException; import java.io.Serializable; @@ -38,51 +42,44 @@ import net.sourceforge.tuned.FileUtilities; import org.w3c.dom.Document; import org.w3c.dom.Node; - public class TheTVDBClient extends AbstractEpisodeListProvider { - + private final String host = "www.thetvdb.com"; - + private final Map mirrors = new EnumMap(MirrorType.class); - + private final String apikey; - - + public TheTVDBClient(String apikey) { if (apikey == null) throw new NullPointerException("apikey must not be null"); - + this.apikey = apikey; } - - + @Override public String getName() { return "TheTVDB"; } - - + @Override public Icon getIcon() { return ResourceManager.getIcon("search.thetvdb"); } - - + @Override public boolean hasSingleSeasonSupport() { return true; } - - + @Override public boolean hasLocaleSupport() { return true; } - - + public String getLanguageCode(Locale locale) { String code = locale.getLanguage(); - + // Java language code => TheTVDB language code if (code.equals("iw")) // Hebrew return "he"; @@ -92,71 +89,73 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return "id"; if (code.equals("ro")) // Russian return "ru"; - + return code; } - - + @Override public ResultCache getCache() { return new ResultCache(host, Cache.getCache("web-datasource")); } - - + @Override public List fetchSearchResult(String query, Locale locale) throws Exception { // perform online search URL url = getResource(MirrorType.SEARCH, "/api/GetSeries.php?seriesname=" + encode(query, true) + "&language=" + getLanguageCode(locale)); Document dom = getDocument(url); - + List nodes = selectNodes("Data/Series", dom); Map resultSet = new LinkedHashMap(); - + for (Node node : nodes) { int sid = getIntegerContent("seriesid", node); String seriesName = getTextContent("SeriesName", node); - + + List aliasNames = new ArrayList(2); + for (Node aliasNode : selectNodes("AliasNames", node)) { + aliasNames.add(getTextContent(aliasNode)); + } + if (!resultSet.containsKey(sid)) { - resultSet.put(sid, new TheTVDBSearchResult(seriesName, sid)); + resultSet.put(sid, new TheTVDBSearchResult(seriesName, aliasNames.toArray(new String[0]), sid)); } } - + return new ArrayList(resultSet.values()); } - - + @Override public List fetchEpisodeList(SearchResult searchResult, SortOrder sortOrder, Locale locale) throws Exception { TheTVDBSearchResult series = (TheTVDBSearchResult) searchResult; Document seriesRecord = getSeriesRecord(series, getLanguageCode(locale)); - + // we could get the series name from the search result, but the language may not match the given parameter String seriesName = selectString("Data/Series/SeriesName", seriesRecord); Date seriesStartDate = Date.parse(selectString("Data/Series/FirstAired", seriesRecord), "yyyy-MM-dd"); - + List nodes = selectNodes("Data/Episode", seriesRecord); - + List episodes = new ArrayList(nodes.size()); List specials = new ArrayList(5); - + for (Node node : nodes) { String episodeName = getTextContent("EpisodeName", node); String dvdSeasonNumber = getTextContent("DVD_season", node); String dvdEpisodeNumber = getTextContent("DVD_episodenumber", node); Integer absoluteNumber = getIntegerContent("absolute_number", node); Date airdate = Date.parse(getTextContent("FirstAired", node), "yyyy-MM-dd"); - + // default numbering Integer episodeNumber = getIntegerContent("EpisodeNumber", node); Integer seasonNumber = getIntegerContent("SeasonNumber", node); - + if (seasonNumber == null || seasonNumber == 0) { // handle as special episode Integer airsBefore = getIntegerContent("airsbefore_season", node); if (airsBefore != null) { seasonNumber = airsBefore; } - + // use given episode number as special number or count specials by ourselves Integer specialNumber = (episodeNumber != null) ? episodeNumber : filterBySeason(specials, seasonNumber).size() + 1; specials.add(new Episode(seriesName, seriesStartDate, seasonNumber, null, episodeName, null, specialNumber, airdate, searchResult)); @@ -175,38 +174,37 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { // ignore, fallback to default numbering } } - + episodes.add(new Episode(seriesName, seriesStartDate, seasonNumber, episodeNumber, episodeName, absoluteNumber, null, airdate, searchResult)); } } - + // episodes my not be ordered by DVD episode number sortEpisodes(episodes); - + // add specials at the end episodes.addAll(specials); - + return episodes; } - - + public Document getSeriesRecord(TheTVDBSearchResult searchResult, String languageCode) throws Exception { URL seriesRecord = getResource(MirrorType.ZIP, "/api/" + apikey + "/series/" + searchResult.getSeriesId() + "/all/" + languageCode + ".zip"); - + try { - + ZipInputStream zipInputStream = new ZipInputStream(seriesRecord.openStream()); ZipEntry zipEntry; - + try { String seriesRecordName = languageCode + ".xml"; - + while ((zipEntry = zipInputStream.getNextEntry()) != null) { if (seriesRecordName.equals(zipEntry.getName())) { return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(zipInputStream); } } - + // zip file must contain the series record throw new FileNotFoundException(String.format("Archive must contain %s: %s", seriesRecordName, seriesRecord)); } finally { @@ -216,20 +214,19 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { throw new FileNotFoundException(String.format("Series record not found: %s [%s]: %s", searchResult.getName(), languageCode, seriesRecord)); } } - - + public TheTVDBSearchResult lookupByID(int id, Locale locale) throws Exception { TheTVDBSearchResult cachedItem = getCache().getData("lookupByID", id, locale, TheTVDBSearchResult.class); if (cachedItem != null) { return cachedItem; } - + try { URL baseRecordLocation = getResource(MirrorType.XML, "/api/" + apikey + "/series/" + id + "/all/" + getLanguageCode(locale) + ".xml"); Document baseRecord = getDocument(baseRecordLocation); - + String name = selectString("//SeriesName", baseRecord); - + TheTVDBSearchResult series = new TheTVDBSearchResult(name, id); getCache().putData("lookupByID", id, locale, series); return series; @@ -239,35 +236,32 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public TheTVDBSearchResult lookupByIMDbID(int imdbid, Locale locale) throws Exception { TheTVDBSearchResult cachedItem = getCache().getData("lookupByIMDbID", imdbid, locale, TheTVDBSearchResult.class); if (cachedItem != null) { return cachedItem; } - + URL query = getResource(null, "/api/GetSeriesByRemoteID.php?imdbid=" + imdbid + "&language=" + getLanguageCode(locale)); Document dom = getDocument(query); - + String id = selectString("//seriesid", dom); String name = selectString("//SeriesName", dom); - + if (id == null || id.isEmpty() || name == null || name.isEmpty()) return null; - + TheTVDBSearchResult series = new TheTVDBSearchResult(name, Integer.parseInt(id)); getCache().putData("lookupByIMDbID", imdbid, locale, series); return series; } - - + @Override public URI getEpisodeListLink(SearchResult searchResult) { return URI.create("http://" + host + "/?tab=seasonall&id=" + ((TheTVDBSearchResult) searchResult).getSeriesId()); } - - + protected String getMirror(MirrorType mirrorType) throws Exception { synchronized (mirrors) { if (mirrors.isEmpty()) { @@ -282,49 +276,48 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } catch (Exception e) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, e.getMessage(), e); } - + // initialize mirrors Document dom = getDocument(getResource(null, "/api/" + apikey + "/mirrors.xml")); - + // all mirrors by type Map> mirrorListMap = new EnumMap>(MirrorType.class); - + // initialize mirror list per type for (MirrorType type : MirrorType.values()) { mirrorListMap.put(type, new ArrayList(5)); } - + // traverse all mirrors for (Node node : selectNodes("Mirrors/Mirror", dom)) { // mirror data String mirror = getTextContent("mirrorpath", node); int typeMask = Integer.parseInt(getTextContent("typemask", node)); - + // add mirror to the according type lists for (MirrorType type : MirrorType.fromTypeMask(typeMask)) { mirrorListMap.get(type).add(mirror); } } - + // put random entry from each type list into mirrors Random random = new Random(); - + for (MirrorType type : MirrorType.values()) { List list = mirrorListMap.get(type); - + if (!list.isEmpty()) { mirrors.put(type, list.get(random.nextInt(list.size()))); } } - + getCache().putData("mirrors", null, null, mirrors); } - + return mirrors.get(mirrorType); } } - - + protected URL getResource(MirrorType mirrorType, String path) throws Exception { if (mirrorType != null) { // use mirror @@ -333,23 +326,20 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return new URL(mirror + path); } } - + // use default server return new URL("http", host, path); } - - + protected static enum MirrorType { XML(1), BANNER(2), ZIP(4), SEARCH(1); - + private final int bitMask; - - + private MirrorType(int bitMask) { this.bitMask = bitMask; } - - + public static EnumSet fromTypeMask(int typeMask) { // initialize enum set with all types EnumSet enumSet = EnumSet.allOf(MirrorType.class); @@ -361,46 +351,42 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } return enumSet; }; - + } - - + public SeriesInfo getSeriesInfoByID(int thetvdbid, Locale locale) throws Exception { return getSeriesInfo(new TheTVDBSearchResult(null, thetvdbid), locale); } - - + public SeriesInfo getSeriesInfoByIMDbID(int imdbid, Locale locale) throws Exception { return getSeriesInfo(lookupByIMDbID(imdbid, locale), locale); } - - + public SeriesInfo getSeriesInfoByName(String name, Locale locale) throws Exception { for (SearchResult it : search(name, locale)) { if (name.equalsIgnoreCase(it.getName())) { return getSeriesInfo((TheTVDBSearchResult) it, locale); } } - + return null; } - - + public SeriesInfo getSeriesInfo(TheTVDBSearchResult searchResult, Locale locale) throws Exception { // check cache first SeriesInfo cachedItem = getCache().getData("seriesInfo", searchResult.seriesId, locale, SeriesInfo.class); if (cachedItem != null) { return cachedItem; } - + Document dom = getDocument(getResource(MirrorType.XML, "/api/" + apikey + "/series/" + searchResult.seriesId + "/" + getLanguageCode(locale) + ".xml")); - + Node node = selectNode("//Series", dom); Map fields = new EnumMap(SeriesProperty.class); - + // remember banner mirror fields.put(SeriesProperty.BannerMirror, getResource(MirrorType.BANNER, "/banners/").toString()); - + // copy values from xml for (SeriesProperty key : SeriesProperty.values()) { String value = getTextContent(key.name(), node); @@ -408,42 +394,36 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { fields.put(key, value); } } - + SeriesInfo seriesInfo = new SeriesInfo(fields); getCache().putData("seriesInfo", searchResult.seriesId, locale, seriesInfo); return seriesInfo; } - - + public static class SeriesInfo implements Serializable { - + public static enum SeriesProperty { id, Actors, Airs_DayOfWeek, Airs_Time, ContentRating, FirstAired, Genre, IMDB_ID, Language, Network, Overview, Rating, RatingCount, Runtime, SeriesName, Status, BannerMirror, banner, fanart, poster } - + protected Map fields; - - + protected SeriesInfo() { // used by serializer } - - + protected SeriesInfo(Map fields) { this.fields = new EnumMap(fields); } - - + public String get(Object key) { return fields.get(SeriesProperty.valueOf(key.toString())); } - - + public String get(SeriesProperty key) { return fields.get(key); } - - + public Integer getId() { // e.g. 80348 try { @@ -452,20 +432,17 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public List getActors() { // e.g. |Zachary Levi|Adam Baldwin|Yvonne Strzechowski| return split(get(SeriesProperty.Actors)); } - - + public List getGenres() { // e.g. |Comedy| return split(get(SeriesProperty.Genre)); } - - + protected List split(String values) { List items = new ArrayList(); if (values != null && values.length() > 0) { @@ -478,37 +455,31 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { } return items; } - - + public String getAirDayOfWeek() { // e.g. Monday return get(SeriesProperty.Airs_DayOfWeek); } - - + public String getAirTime() { // e.g. 8:00 PM return get(SeriesProperty.Airs_Time); } - - + public Date getFirstAired() { // e.g. 2007-09-24 return Date.parse(get(SeriesProperty.FirstAired), "yyyy-MM-dd"); } - - + public String getContentRating() { // e.g. TV-PG return get(SeriesProperty.ContentRating); } - - + public String getCertification() { return getContentRating(); // another getter for compability reasons } - - + public Integer getImdbId() { // e.g. tt0934814 try { @@ -517,8 +488,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public Locale getLanguage() { // e.g. en try { @@ -527,14 +497,12 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public String getOverview() { // e.g. Zachary Levi (Less Than Perfect) plays Chuck... return get(SeriesProperty.Overview); } - - + public Double getRating() { // e.g. 9.0 try { @@ -543,8 +511,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public Integer getRatingCount() { // e.g. 696 try { @@ -553,32 +520,27 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public String getRuntime() { // e.g. 30 return get(SeriesProperty.Runtime); } - - + public String getName() { // e.g. Chuck return get(SeriesProperty.SeriesName); } - - + public String getNetwork() { // e.g. CBS return get(SeriesProperty.Network); } - - + public String getStatus() { // e.g. Continuing return get(SeriesProperty.Status); } - - + public URL getBannerMirrorUrl() { try { return new URL(get(BannerProperty.BannerMirror)); @@ -586,8 +548,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public URL getBannerUrl() throws MalformedURLException { try { return new URL(getBannerMirrorUrl(), get(SeriesProperty.banner)); @@ -595,8 +556,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public URL getFanartUrl() { try { return new URL(getBannerMirrorUrl(), get(SeriesProperty.fanart)); @@ -604,8 +564,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public URL getPosterUrl() { try { return new URL(getBannerMirrorUrl(), get(SeriesProperty.poster)); @@ -613,15 +572,13 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + @Override public String toString() { return fields.toString(); } } - - + /** * Search for a series banner matching the given parameters * @@ -634,37 +591,36 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { filter.put(BannerProperty.valueOf(it.getKey().toString()), it.getValue().toString()); } } - + // search for a banner matching the selector for (BannerDescriptor it : getBannerList(series)) { if (it.fields.entrySet().containsAll(filter.entrySet())) { return it; } } - + return null; } - - + public List getBannerList(TheTVDBSearchResult series) throws Exception { // check cache first BannerDescriptor[] cachedList = getCache().getData("banners", series.seriesId, null, BannerDescriptor[].class); if (cachedList != null) { return asList(cachedList); } - + Document dom = getDocument(getResource(MirrorType.XML, "/api/" + apikey + "/series/" + series.seriesId + "/banners.xml")); - + List nodes = selectNodes("//Banner", dom); List banners = new ArrayList(); - + for (Node node : nodes) { try { Map 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()) { String value = getTextContent(key.name(), node); @@ -672,48 +628,42 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { item.put(key, value); } } - + banners.add(new BannerDescriptor(item)); } catch (Exception e) { // log and ignore Logger.getLogger(getClass().getName()).log(Level.WARNING, "Invalid banner descriptor", e); } } - + getCache().putData("banners", series.seriesId, null, banners.toArray(new BannerDescriptor[0])); return banners; } - - + public static class BannerDescriptor implements Serializable { - + public static enum BannerProperty { id, BannerMirror, BannerPath, BannerType, BannerType2, Season, Colors, Language, Rating, RatingCount, SeriesName, ThumbnailPath, VignettePath } - + protected Map fields; - - + protected BannerDescriptor() { // used by serializer } - - + protected BannerDescriptor(Map fields) { this.fields = new EnumMap(fields); } - - + public String get(Object key) { return fields.get(BannerProperty.valueOf(key.toString())); } - - + public String get(BannerProperty key) { return fields.get(key); } - - + public URL getBannerMirrorUrl() throws MalformedURLException { try { return new URL(get(BannerProperty.BannerMirror)); @@ -721,8 +671,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public URL getUrl() throws MalformedURLException { try { return new URL(getBannerMirrorUrl(), get(BannerProperty.BannerPath)); @@ -730,13 +679,11 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public String getExtension() { return FileUtilities.getExtension(get(BannerProperty.BannerPath)); } - - + public Integer getId() { try { return new Integer(get(BannerProperty.id)); @@ -744,18 +691,15 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public String getBannerType() { return get(BannerProperty.BannerType); } - - + public String getBannerType2() { return get(BannerProperty.BannerType2); } - - + public Integer getSeason() { try { return new Integer(get(BannerProperty.Season)); @@ -763,13 +707,11 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public String getColors() { return get(BannerProperty.Colors); } - - + public Locale getLocale() { try { return new Locale(get(BannerProperty.Language)); @@ -777,8 +719,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public Double getRating() { try { return new Double(get(BannerProperty.Rating)); @@ -786,8 +727,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public Integer getRatingCount() { try { return new Integer(get(BannerProperty.RatingCount)); @@ -795,13 +735,11 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public boolean hasSeriesName() { return Boolean.parseBoolean(get(BannerProperty.SeriesName)); } - - + public URL getThumbnailUrl() throws MalformedURLException { try { return new URL(getBannerMirrorUrl(), get(BannerProperty.ThumbnailPath)); @@ -809,8 +747,7 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + public URL getVignetteUrl() throws MalformedURLException { try { return new URL(getBannerMirrorUrl(), get(BannerProperty.VignettePath)); @@ -818,12 +755,11 @@ public class TheTVDBClient extends AbstractEpisodeListProvider { return null; } } - - + @Override public String toString() { return fields.toString(); } } - + } diff --git a/source/net/sourceforge/filebot/web/TheTVDBSearchResult.java b/source/net/sourceforge/filebot/web/TheTVDBSearchResult.java index e31febed..e2212883 100644 --- a/source/net/sourceforge/filebot/web/TheTVDBSearchResult.java +++ b/source/net/sourceforge/filebot/web/TheTVDBSearchResult.java @@ -1,46 +1,43 @@ - package net.sourceforge.filebot.web; public class TheTVDBSearchResult extends SearchResult { - + protected int seriesId; - - + protected TheTVDBSearchResult() { // used by serializer } - - + public TheTVDBSearchResult(String seriesName, int seriesId) { - super(seriesName); + this(seriesName, new String[0], seriesId); + } + + public TheTVDBSearchResult(String seriesName, String[] aliasNames, int seriesId) { + super(seriesName, aliasNames); this.seriesId = seriesId; } - - + public int getId() { return seriesId; } - - + public int getSeriesId() { return seriesId; } - - + @Override public int hashCode() { return seriesId; } - - + @Override public boolean equals(Object object) { if (object instanceof TheTVDBSearchResult) { TheTVDBSearchResult other = (TheTVDBSearchResult) object; return this.seriesId == other.seriesId; } - + return false; } } \ No newline at end of file