diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java index fb2e574d..e594061f 100644 --- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java +++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java @@ -1,7 +1,5 @@ - package net.sourceforge.filebot.cli; - import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.addAll; @@ -67,7 +65,6 @@ import java.util.AbstractMap.SimpleImmutableEntry; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -121,9 +118,8 @@ import net.sourceforge.filebot.web.SubtitleProvider; import net.sourceforge.filebot.web.VideoHashSubtitleService; import net.sourceforge.tuned.FileUtilities.ParentFilter; - public class CmdlineOperations implements CmdlineInterface { - + @Override public List rename(Collection files, RenameAction action, String conflict, String output, String formatExpression, String db, String query, String sortOrder, String filterExpression, String lang, boolean strict) throws Exception { ExpressionFormat format = (formatExpression != null) ? new ExpressionFormat(formatExpression) : null; @@ -131,40 +127,40 @@ public class CmdlineOperations implements CmdlineInterface { File outputDir = (output != null && output.length() > 0) ? new File(output).getAbsoluteFile() : null; Locale locale = getLanguage(lang).toLocale(); ConflictAction conflictAction = ConflictAction.forName(conflict); - + if (getEpisodeListProvider(db) != null) { // tv series mode return renameSeries(files, action, conflictAction, outputDir, format, getEpisodeListProvider(db), query, SortOrder.forName(sortOrder), filter, locale, strict); } - + if (getMovieIdentificationService(db) != null) { // movie mode - return renameMovie(files, action, conflictAction, outputDir, format, getMovieIdentificationService(db), query, locale, strict); + return renameMovie(files, action, conflictAction, outputDir, format, getMovieIdentificationService(db), query, filter, locale, strict); } - + if (getMusicIdentificationService(db) != null || containsOnly(files, AUDIO_FILES)) { // music mode return renameMusic(files, action, conflictAction, outputDir, format, getMusicIdentificationService(db) == null ? AcoustID : getMusicIdentificationService(db)); } - + // auto-determine mode List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); double max = mediaFiles.size(); int sxe = 0; // SxE int cws = 0; // common word sequence - + SeriesNameMatcher nameMatcher = new SeriesNameMatcher(locale); Collection cwsList = emptySet(); if (max >= 5) { cwsList = nameMatcher.matchAll(mediaFiles.toArray(new File[0])); } - + for (File f : mediaFiles) { // count SxE matches if (MediaDetection.getEpisodeIdentifier(f.getName(), true) != null) { sxe++; } - + // count CWS matches for (String base : cwsList) { if (base.equalsIgnoreCase(nameMatcher.matchByFirstCommonWordSequence(base, f.getName()))) { @@ -173,31 +169,30 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + CLILogger.finest(format("Filename pattern: [%.02f] SxE, [%.02f] CWS", sxe / max, cws / max)); if (sxe > (max * 0.65) || cws > (max * 0.65)) { return renameSeries(files, action, conflictAction, outputDir, format, WebServices.TheTVDB, query, SortOrder.forName(sortOrder), filter, locale, strict); // use default episode db } else { - return renameMovie(files, action, conflictAction, outputDir, format, WebServices.TMDb, query, locale, strict); // use default movie db + return renameMovie(files, action, conflictAction, outputDir, format, WebServices.TMDb, query, filter, locale, strict); // use default movie db } } - - + public List renameSeries(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, EpisodeListProvider db, String query, SortOrder sortOrder, ExpressionFilter filter, Locale locale, boolean strict) throws Exception { CLILogger.config(format("Rename episodes using [%s]", db.getName())); - + List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); if (mediaFiles.isEmpty()) { throw new Exception("No media files: " + files); } - + // similarity metrics for matching List> matches = new ArrayList>(); - + // auto-determine optimal batch sets for (Entry, Set> sameSeriesGroup : mapSeriesNamesByFiles(mediaFiles, locale).entrySet()) { List> batchSets = new ArrayList>(); - + if (sameSeriesGroup.getValue() != null && sameSeriesGroup.getValue().size() > 0) { // handle series name batch set all at once batchSets.add(new ArrayList(sameSeriesGroup.getKey())); @@ -205,50 +200,40 @@ public class CmdlineOperations implements CmdlineInterface { // these files don't seem to belong to any series -> handle folder per folder batchSets.addAll(mapByFolder(sameSeriesGroup.getKey()).values()); } - + for (List batch : batchSets) { // auto-detect series name if not given Collection seriesNames = (query == null) ? detectSeriesQuery(batch, locale) : asList(query.split("[|]")); - + if (strict && seriesNames.size() > 1) { throw new Exception("Handling multiple shows requires non-strict matching"); } - + // fetch episode data - Set episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict); - + Collection episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict); + if (episodes.size() == 0) { CLILogger.warning("Failed to fetch episode data: " + seriesNames); continue; } - + // filter episodes - if (filter != null) { - CLILogger.fine(String.format("Apply Filter: {%s}", filter.getExpression())); - for (Iterator itr = episodes.iterator(); itr.hasNext();) { - Episode episode = itr.next(); - if (filter.matches(new MediaBindingBean(episode, null, null))) { - CLILogger.finest(String.format("Include [%s]", episode)); - } else { - itr.remove(); - } - } - } - + episodes = applyExpressionFilter(episodes, filter); + matches.addAll(matchEpisodes(filter(batch, VIDEO_FILES), episodes, strict)); matches.addAll(matchEpisodes(filter(batch, SUBTITLE_FILES), episodes, strict)); } } - + if (matches.isEmpty()) { throw new Exception("Unable to match files to episode data"); } - + // handle derived files List> derivateMatches = new ArrayList>(); SortedSet derivateFiles = new TreeSet(files); derivateFiles.removeAll(mediaFiles); - + for (File file : derivateFiles) { for (Match match : matches) { if (file.getParentFile().equals(match.getValue().getParentFile()) && isDerived(file, match.getValue()) && match.getCandidate() instanceof Episode) { @@ -257,10 +242,10 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + // add matches from other files that are linked via filenames matches.addAll(derivateMatches); - + // first write all the metadata if xattr is enabled if (useExtendedFileAttributes()) { try { @@ -273,49 +258,47 @@ public class CmdlineOperations implements CmdlineInterface { CLILogger.warning("Failed to write xattr: " + e.getMessage()); } } - + // map old files to new paths by applying formatting and validating filenames Map renameMap = new LinkedHashMap(); - + for (Match match : matches) { File file = match.getValue(); Object episode = match.getCandidate(); String newName = (format != null) ? format.format(new MediaBindingBean(episode, file, getContext(matches))) : validateFileName(EpisodeFormat.SeasonEpisode.format(episode)); - + renameMap.put(file, getDestinationFile(file, newName, outputDir)); } - + // rename episodes Analytics.trackEvent("CLI", "Rename", "Episode", renameMap.size()); return renameAll(renameMap, renameAction, conflictAction); } - - + private List> matchEpisodes(Collection files, Collection episodes, boolean strict) throws Exception { // always use strict fail-fast matcher EpisodeMatcher matcher = new EpisodeMatcher(files, episodes, strict); List> matches = matcher.match(); - + for (File failedMatch : matcher.remainingValues()) { CLILogger.warning("No matching episode: " + failedMatch.getName()); } - + return matches; } - - + private Set fetchEpisodeSet(final EpisodeListProvider db, final Collection names, final SortOrder sortOrder, final Locale locale, final boolean strict) throws Exception { Set shows = new LinkedHashSet(); Set episodes = new LinkedHashSet(); - + // detect series names and create episode list fetch tasks for (String query : names) { List results = db.search(query, locale); - + // select search result if (results.size() > 0) { List selectedSearchResults = selectSearchResult(query, results, strict); - + if (selectedSearchResults != null) { for (SearchResult it : selectedSearchResults) { if (shows.add(it)) { @@ -331,25 +314,24 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + return episodes; } - - - public List renameMovie(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, MovieIdentificationService service, String query, Locale locale, boolean strict) throws Exception { + + public List renameMovie(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, MovieIdentificationService service, String query, ExpressionFilter filter, Locale locale, boolean strict) throws Exception { CLILogger.config(format("Rename movies using [%s]", service.getName())); - + // ignore sample files List fileset = filter(files, not(getClutterFileFilter())); - + // handle movie files Set movieFiles = new TreeSet(filter(fileset, VIDEO_FILES)); Set nfoFiles = new TreeSet(filter(fileset, NFO_FILES)); - + List orphanedFiles = new ArrayList(filter(fileset, FILES)); orphanedFiles.removeAll(movieFiles); orphanedFiles.removeAll(nfoFiles); - + Map> derivatesByMovieFile = new HashMap>(); for (File movieFile : movieFiles) { derivatesByMovieFile.put(movieFile, new ArrayList()); @@ -366,7 +348,7 @@ public class CmdlineOperations implements CmdlineInterface { for (List derivates : derivatesByMovieFile.values()) { orphanedFiles.removeAll(derivates); } - + // match movie hashes online final Map movieByFile = new TreeMap(); if (query == null) { @@ -379,10 +361,10 @@ public class CmdlineOperations implements CmdlineInterface { } Analytics.trackEvent(service.getName(), "HashLookup", "Movie", hashLookup.size()); // number of positive hash lookups } catch (UnsupportedOperationException e) { - + // ignore logging => hash lookup only supported by OpenSubtitles } } - + // collect useful nfo files even if they are not part of the selected fileset Set effectiveNfoFileSet = new TreeSet(nfoFiles); for (File dir : mapByFolder(movieFiles).keySet()) { @@ -391,20 +373,20 @@ public class CmdlineOperations implements CmdlineInterface { for (File dir : filter(fileset, FOLDERS)) { addAll(effectiveNfoFileSet, dir.listFiles(NFO_FILES)); } - + for (File nfo : effectiveNfoFileSet) { try { Movie movie = grepMovie(nfo, service, locale); - + // ignore illegal nfos if (movie == null) { continue; } - + if (nfoFiles.contains(nfo)) { movieByFile.put(nfo, movie); } - + if (isDiskFolder(nfo.getParentFile())) { // special handling for disk folders for (File folder : fileset) { @@ -416,7 +398,7 @@ public class CmdlineOperations implements CmdlineInterface { // match movie info to movie files that match the nfo file name SortedSet siblingMovieFiles = new TreeSet(filter(movieFiles, new ParentFilter(nfo.getParentFile()))); String baseName = stripReleaseInfo(getName(nfo)).toLowerCase(); - + for (File movieFile : siblingMovieFiles) { if (!baseName.isEmpty() && stripReleaseInfo(getName(movieFile)).toLowerCase().startsWith(baseName)) { movieByFile.put(movieFile, movie); @@ -429,64 +411,72 @@ public class CmdlineOperations implements CmdlineInterface { } } else { CLILogger.fine(format("Looking up movie by query [%s]", query)); - Movie result = (Movie) selectSearchResult(query, service.searchMovie(query, locale), strict).get(0); + List results = service.searchMovie(query, locale); + results = applyExpressionFilter(results, filter); + + if (results.isEmpty()) { + throw new Exception(format("Failed to look up movie by query [%s]", query)); + } + // force all mappings + Movie result = (Movie) selectSearchResult(query, results, strict).get(0); for (File file : files) { movieByFile.put(file, result); } } - + // collect files that will be matched one by one List movieMatchFiles = new ArrayList(); movieMatchFiles.addAll(movieFiles); movieMatchFiles.addAll(nfoFiles); movieMatchFiles.addAll(filter(files, FOLDERS)); movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files - + // sanity check that we have something to do if (fileset.isEmpty() || movieMatchFiles.isEmpty()) { throw new Exception("No media files: " + files); } - + // map movies to (possibly multiple) files (in natural order) Map> filesByMovie = new HashMap>(); - + // map all files by movie for (final File file : movieMatchFiles) { Movie movie = movieByFile.get(file); - + // unknown hash, try via imdb id from nfo file if (movie == null) { CLILogger.fine(format("Auto-detect movie from context: [%s]", file)); Collection results = detectMovie(file, null, service, locale, strict); + results = applyExpressionFilter(results, filter); try { movie = (Movie) selectSearchResult(query, results, strict).get(0); } catch (Exception e) { CLILogger.log(Level.WARNING, String.format("%s: [%s/%s] %s", e.getClass().getSimpleName(), guessMovieFolder(file) != null ? guessMovieFolder(file).getName() : null, file.getName(), e.getMessage())); } - + if (movie != null) { Analytics.trackEvent(service.getName(), "SearchMovie", movie.toString(), 1); } } - + // check if we managed to lookup the movie descriptor if (movie != null) { // get file list for movie SortedSet movieParts = filesByMovie.get(movie); - + if (movieParts == null) { movieParts = new TreeSet(); filesByMovie.put(movie, movieParts); } - + movieParts.add(file); } } - + // collect all File/MoviePart matches List> matches = new ArrayList>(); - + for (Entry> entry : filesByMovie.entrySet()) { for (List fileSet : mapByExtension(entry.getValue()).values()) { // resolve movie parts @@ -495,9 +485,9 @@ public class CmdlineOperations implements CmdlineInterface { if (fileSet.size() > 1) { moviePart = new MoviePart(moviePart, i + 1, fileSet.size()); } - + matches.add(new Match(fileSet.get(i), moviePart.clone())); - + // automatically add matches for derivate files List derivates = derivatesByMovieFile.get(fileSet.get(i)); if (derivates != null) { @@ -508,7 +498,7 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + // first write all the metadata if xattr is enabled if (useExtendedFileAttributes()) { try { @@ -521,28 +511,27 @@ public class CmdlineOperations implements CmdlineInterface { CLILogger.warning("Failed to write xattr: " + e.getMessage()); } } - + // map old files to new paths by applying formatting and validating filenames Map renameMap = new LinkedHashMap(); - + for (Match match : matches) { File file = match.getValue(); Object movie = match.getCandidate(); String newName = (format != null) ? format.format(new MediaBindingBean(movie, file, getContext(matches))) : validateFileName(MovieFormat.NameYear.format(movie)); - + renameMap.put(file, getDestinationFile(file, newName, outputDir)); } - + // rename movies Analytics.trackEvent("CLI", "Rename", "Movie", renameMap.size()); return renameAll(renameMap, renameAction, conflictAction); } - - + public List renameMusic(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, MusicIdentificationService service) throws Exception { CLILogger.config(format("Rename music using [%s]", service.getName())); List audioFiles = filter(files, AUDIO_FILES); - + // check audio files against acoustid List> matches = new ArrayList>(); for (Entry it : service.lookup(audioFiles).entrySet()) { @@ -550,18 +539,18 @@ public class CmdlineOperations implements CmdlineInterface { matches.add(new Match(it.getKey(), it.getValue().clone())); } } - + // map old files to new paths by applying formatting and validating filenames Map renameMap = new LinkedHashMap(); - + for (Match it : matches) { File file = it.getValue(); AudioTrack music = (AudioTrack) it.getCandidate(); String newName = (format != null) ? format.format(new MediaBindingBean(music, file, getContext(matches))) : validateFileName(music.toString()); - + renameMap.put(file, getDestinationFile(file, newName, outputDir)); } - + // error logging if (renameMap.size() != audioFiles.size()) { for (File f : audioFiles) { @@ -570,16 +559,15 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + // rename movies Analytics.trackEvent("CLI", "Rename", "AudioTrack", renameMap.size()); return renameAll(renameMap, renameAction, conflictAction); } - - + private Map getContext(final Collection> matches) { return new AbstractMap() { - + @Override public Set> entrySet() { Set> context = new LinkedHashSet>(); @@ -592,54 +580,52 @@ public class CmdlineOperations implements CmdlineInterface { } }; } - - + private File getDestinationFile(File original, String newName, File outputDir) { String extension = getExtension(original); File newFile = new File(extension != null ? newName + '.' + extension : newName); - + // resolve against output dir if (outputDir != null && !newFile.isAbsolute()) { newFile = new File(outputDir, newFile.getPath()); } - + if (isInvalidFilePath(newFile) && !isUnixFS()) { CLILogger.config("Stripping invalid characters from new path: " + newName); newFile = validateFilePath(newFile); } - + return newFile; } - - + public List renameAll(Map renameMap, RenameAction renameAction, ConflictAction conflictAction) throws Exception { // rename files final List> renameLog = new ArrayList>(); - + try { for (Entry it : renameMap.entrySet()) { try { File source = it.getKey(); File destination = it.getValue(); - + // resolve destination if (!destination.isAbsolute()) { // same folder, different name destination = new File(source.getParentFile(), destination.getPath()); } - + if (!destination.equals(source) && destination.exists()) { if (conflictAction == ConflictAction.FAIL) { throw new Exception("File already exists: " + destination); } - + if (conflictAction == ConflictAction.OVERRIDE) { if (!destination.delete()) { throw new Exception("Failed to override file: " + destination); } } } - + // rename file, throw exception on failure if (!destination.equals(source) && !destination.exists()) { CLILogger.info(format("[%s] Rename [%s] to [%s]", renameAction, it.getKey(), it.getValue())); @@ -647,7 +633,7 @@ public class CmdlineOperations implements CmdlineInterface { } else { CLILogger.info(format("Skipped [%s] because [%s] already exists", source, destination)); } - + // remember successfully renamed matches for history entry and possible revert renameLog.add(new SimpleImmutableEntry(source, destination)); } catch (IOException e) { @@ -659,48 +645,47 @@ public class CmdlineOperations implements CmdlineInterface { if (renameLog.size() > 0) { // update rename history HistorySpooler.getInstance().append(renameMap.entrySet()); - + // printer number of renamed files if any CLILogger.fine(format("Processed %d files", renameLog.size())); } } - + // new file names List destinationList = new ArrayList(); for (Entry it : renameLog) { destinationList.add(it.getValue()); } - + return destinationList; } - - + @Override public List getSubtitles(Collection files, String db, String query, String languageName, String output, String csn, boolean strict) throws Exception { final Language language = getLanguage(languageName); final Pattern databaseFilter = (db != null) ? Pattern.compile(db, Pattern.CASE_INSENSITIVE) : null; CLILogger.finest(String.format("Get [%s] subtitles for %d files", language.getName(), files.size())); - + // when rewriting subtitles to target format an encoding must be defined, default to UTF-8 final Charset outputEncoding = (csn != null) ? Charset.forName(csn) : (output != null) ? Charset.forName("UTF-8") : null; final SubtitleFormat outputFormat = (output != null) ? getSubtitleFormatByName(output) : null; - + // try to find subtitles for each video file List remainingVideos = new ArrayList(filter(files, VIDEO_FILES)); - + // parallel download List subtitleFiles = new ArrayList(); - + if (remainingVideos.isEmpty()) { throw new Exception("No video files: " + files); } - + // lookup subtitles by hash for (VideoHashSubtitleService service : WebServices.getVideoHashSubtitleServices()) { if (remainingVideos.isEmpty() || (databaseFilter != null && !databaseFilter.matcher(service.getName()).matches())) { continue; } - + try { CLILogger.fine("Looking up subtitles by filehash via " + service.getName()); Map subtitles = lookupSubtitleByHash(service, language, remainingVideos); @@ -711,17 +696,17 @@ public class CmdlineOperations implements CmdlineInterface { CLILogger.warning("Lookup by hash failed: " + e.getMessage()); } } - + // lookup subtitles via text search, only perform hash lookup in strict mode if (!remainingVideos.isEmpty()) { // auto-detect search query Set querySet = new TreeSet(String.CASE_INSENSITIVE_ORDER); - + if (query == null) { try { List videoFiles = filter(files, VIDEO_FILES); querySet.addAll(detectSeriesNames(videoFiles, language.toLocale())); - + // auto-detect movie names for (File f : videoFiles) { if (!isEpisode(f.getName(), false)) { @@ -733,19 +718,19 @@ public class CmdlineOperations implements CmdlineInterface { } catch (Exception e) { CLILogger.warning("Movie detection failed: " + e.getMessage()); } - + if (querySet.isEmpty()) { throw new Exception("Failed to auto-detect query"); } } else { querySet.add(query); } - + for (SubtitleProvider service : WebServices.getSubtitleProviders()) { if (remainingVideos.isEmpty() || (databaseFilter != null && !databaseFilter.matcher(service.getName()).matches())) { continue; } - + try { CLILogger.fine(format("Searching for %s at [%s]", querySet, service.getName())); Map subtitles = lookupSubtitleByFileName(service, querySet, language, remainingVideos, strict); @@ -757,7 +742,7 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + // no subtitles for remaining video files for (File it : remainingVideos) { CLILogger.warning("No matching subtitles found: " + it); @@ -767,19 +752,17 @@ public class CmdlineOperations implements CmdlineInterface { } return subtitleFiles; } - - + @Override public List getMissingSubtitles(Collection files, String db, String query, final String languageName, String output, String csn, boolean strict) throws Exception { List videoFiles = filter(filter(files, VIDEO_FILES), new FileFilter() { - + // save time on repeating filesystem calls private final Map cache = new HashMap(); - + // get language code suffix for given language (.eng) private final String languageCodeSuffix = "." + Language.getISO3LanguageCodeByName(getLanguage(languageName).getName()); - - + @Override public boolean accept(File video) { File[] subtitlesByFolder = cache.get(video.getParentFile()); @@ -787,28 +770,27 @@ public class CmdlineOperations implements CmdlineInterface { subtitlesByFolder = video.getParentFile().listFiles(SUBTITLE_FILES); cache.put(video.getParentFile(), subtitlesByFolder); } - + for (File subtitle : subtitlesByFolder) { if (isDerived(subtitle, video) && (subtitle.getName().contains(languageCodeSuffix))) return false; } - + return true; } }); - + if (videoFiles.isEmpty()) { CLILogger.info("No missing subtitles"); return emptyList(); } - + return getSubtitles(videoFiles, db, query, languageName, output, csn, strict); } - - + private Map downloadSubtitleBatch(String service, Map subtitles, SubtitleFormat outputFormat, Charset outputEncoding) { Map downloads = new HashMap(); - + // fetch subtitle for (Entry it : subtitles.entrySet()) { try { @@ -818,56 +800,53 @@ public class CmdlineOperations implements CmdlineInterface { CLILogger.warning(format("Failed to download %s: %s", it.getValue().getPath(), e.getMessage())); } } - + return downloads; } - - + private File downloadSubtitle(SubtitleDescriptor descriptor, File movieFile, SubtitleFormat outputFormat, Charset outputEncoding) throws Exception { // fetch subtitle archive CLILogger.config(format("Fetching [%s]", descriptor.getPath())); MemoryFile subtitleFile = fetchSubtitle(descriptor); - + // subtitle filename is based on movie filename String base = getName(movieFile); String ext = getExtension(subtitleFile.getName()); ByteBuffer data = subtitleFile.getData(); - + if (outputFormat != null || outputEncoding != null) { if (outputFormat != null) { ext = outputFormat.getFilter().extension(); // adjust extension of the output file } - + CLILogger.finest(format("Export [%s] as: %s / %s", subtitleFile.getName(), outputFormat, outputEncoding.displayName(Locale.ROOT))); data = exportSubtitles(subtitleFile, outputFormat, 0, outputEncoding); } - + File destination = new File(movieFile.getParentFile(), formatSubtitle(base, descriptor.getLanguageName(), ext)); CLILogger.info(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName())); - + writeFile(data, destination); return destination; } - - + private Map lookupSubtitleByHash(VideoHashSubtitleService service, Language language, Collection videoFiles) throws Exception { Map subtitleByVideo = new HashMap(videoFiles.size()); - + for (Entry> it : service.getSubtitleList(videoFiles.toArray(new File[0]), language.getName()).entrySet()) { if (it.getValue() != null && it.getValue().size() > 0) { CLILogger.finest(format("Matched [%s] to [%s] via filehash", it.getKey().getName(), it.getValue().get(0).getName())); subtitleByVideo.put(it.getKey(), it.getValue().get(0)); } } - + return subtitleByVideo; } - - + private Map lookupSubtitleByFileName(SubtitleProvider service, Collection querySet, Language language, Collection videoFiles, boolean strict) throws Exception { // search for subtitles List subtitles = findSubtitles(service, querySet, language.getName()); - + // match subtitle files to video files if (subtitles.size() > 0) { Map subtitleByVideo = matchSubtitles(videoFiles, subtitles, strict); @@ -876,31 +855,45 @@ public class CmdlineOperations implements CmdlineInterface { } return subtitleByVideo; } - + return emptyMap(); } - - + private List detectSeriesQuery(Collection mediaFiles, Locale locale) throws Exception { // detect series name by common word sequence List names = detectSeriesNames(mediaFiles, locale); - + if (names.isEmpty()) { throw new Exception("Failed to auto-detect query"); } - + CLILogger.config("Auto-detected query: " + names); return names; } - - + + private List applyExpressionFilter(Collection input, ExpressionFilter filter) throws Exception { + if (filter == null) { + return new ArrayList(input); + } + + CLILogger.fine(String.format("Apply Filter: {%s}", filter.getExpression())); + List output = new ArrayList(input.size()); + for (T it : input) { + if (filter.matches(new MediaBindingBean(it, null, null))) { + CLILogger.finest(String.format("Include [%s]", it)); + output.add(it); + } + } + return output; + } + public List findProbableMatches(final String query, Collection searchResults, boolean strict) { // auto-select most probable search result List probableMatches = new ArrayList(); - + // use name similarity metric final SimilarityMetric metric = new NameSimilarityMetric(); - + // find probable matches using name similarity > 0.8 (or > 0.6 in non-strict mode) for (SearchResult result : searchResults) { float f = (query == null) ? 1 : metric.getSimilarity(query, result.getName()); @@ -910,24 +903,23 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + // sort results by similarity to query if (query != null) { sort(probableMatches, new SimilarityComparator(query)); } return probableMatches; } - - + public List selectSearchResult(String query, Collection searchResults, boolean strict) throws Exception { List probableMatches = findProbableMatches(query, searchResults, strict); - + if (probableMatches.isEmpty() || (strict && probableMatches.size() != 1)) { // allow single search results to just pass through in non-strict mode even if match confidence is low if (searchResults.size() == 1 && !strict) { return new ArrayList(searchResults); } - + if (strict) { throw new Exception("Multiple options: Force auto-select requires non-strict matching: " + searchResults); } else { @@ -938,43 +930,40 @@ public class CmdlineOperations implements CmdlineInterface { } } } - + // return first and only value return probableMatches.size() <= 5 ? probableMatches : probableMatches.subList(0, 5); // trust that the correct match is in the Top 3 } - - + private Language getLanguage(String lang) throws Exception { // try to look up by language code Language language = Language.getLanguage(lang); - + if (language == null) { // try too look up by language name language = Language.getLanguageByName(lang); - + if (language == null) { // unable to lookup language throw new Exception("Illegal language code: " + lang); } } - + return language; } - - + @Override public boolean check(Collection files) throws Exception { // only check existing hashes boolean result = true; - + for (File it : filter(files, MediaTypes.getDefaultFilter("verification"))) { result &= check(it, it.getParentFile()); } - + return result; } - - + @Override public File compute(Collection files, String output, String csn) throws Exception { // check common parent for all given files @@ -982,15 +971,15 @@ public class CmdlineOperations implements CmdlineInterface { for (File it : files) { if (root == null || root.getPath().startsWith(it.getParent())) root = it.getParentFile(); - + if (!it.getParent().startsWith(root.getPath())) throw new Exception("Paths don't share a common root: " + files); } - + // create verification file File outputFile; HashType hashType; - + if (output != null && getExtension(output) != null) { // use given filename hashType = getHashTypeByExtension(getExtension(output)); @@ -1000,40 +989,39 @@ public class CmdlineOperations implements CmdlineInterface { hashType = (output != null) ? getHashTypeByExtension(output) : HashType.SFV; outputFile = new File(root, root.getName() + "." + hashType.getFilter().extension()); } - + if (hashType == null) { throw new Exception("Illegal output type: " + output); } - + CLILogger.config("Using output file: " + outputFile); compute(root.getPath(), files, outputFile, hashType, csn); - + return outputFile; } - - + private boolean check(File verificationFile, File root) throws Exception { HashType type = getHashType(verificationFile); - + // check if type is supported if (type == null) { throw new Exception("Unsupported format: " + verificationFile); } - + // add all file names from verification file CLILogger.fine(format("Checking [%s]", verificationFile.getName())); VerificationFileReader parser = new VerificationFileReader(createTextReader(verificationFile), type.getFormat()); boolean status = true; - + try { while (parser.hasNext()) { try { Entry it = parser.next(); - + File file = new File(root, it.getKey().getPath()).getAbsoluteFile(); String current = computeHash(new File(root, it.getKey().getPath()), type); CLILogger.info(format("%s %s", current, file)); - + if (current.compareToIgnoreCase(it.getValue()) != 0) { throw new IOException(format("Corrupted file found: %s [hash mismatch: %s vs %s]", it.getKey(), current, it.getValue())); } @@ -1045,25 +1033,24 @@ public class CmdlineOperations implements CmdlineInterface { } finally { parser.close(); } - + return status; } - - + private void compute(String root, Collection files, File outputFile, HashType hashType, String csn) throws IOException, Exception { // compute hashes recursively and write to file VerificationFileWriter out = new VerificationFileWriter(outputFile, hashType.getFormat(), csn != null ? csn : "UTF-8"); - + try { CLILogger.fine("Computing hashes"); for (File it : files) { if (it.isHidden() || MediaTypes.getDefaultFilter("verification").accept(it)) continue; - + String relativePath = normalizePathSeparators(it.getPath().replace(root, "")).substring(1); String hash = computeHash(it, hashType); CLILogger.info(format("%s %s", hash, relativePath)); - + out.write(relativePath, hash); } } catch (Exception e) { @@ -1073,46 +1060,43 @@ public class CmdlineOperations implements CmdlineInterface { out.close(); } } - - + @Override public List fetchEpisodeList(String query, String expression, String db, String sortOrderName, String languageName) throws Exception { if (query == null || query.isEmpty()) throw new IllegalArgumentException("query is not defined"); - + // find series on the web and fetch episode list ExpressionFormat format = (expression != null) ? new ExpressionFormat(expression) : null; EpisodeListProvider service = (db == null) ? TheTVDB : getEpisodeListProvider(db); SortOrder sortOrder = SortOrder.forName(sortOrderName); Locale locale = getLanguage(languageName).toLocale(); - + SearchResult hit = selectSearchResult(query, service.search(query, locale), false).get(0); List episodes = new ArrayList(); - + for (Episode it : service.getEpisodeList(hit, sortOrder, locale)) { String name = (format != null) ? format.format(new MediaBindingBean(it, null, null)) : EpisodeFormat.SeasonEpisode.format(it); episodes.add(name); } - + return episodes; } - - + @Override public String getMediaInfo(File file, String expression) throws Exception { ExpressionFormat format = new ExpressionFormat(expression != null ? expression : "{fn} [{resolution} {af} {vc} {ac}]"); return format.format(new MediaBindingBean(file, file, null)); } - - + @Override public List extract(Collection files, String output, String conflict, FileFilter filter, boolean forceExtractAll) throws Exception { ConflictAction conflictAction = ConflictAction.forName(conflict); - + // only keep single-volume archives or first part of multi-volume archives List archiveFiles = filter(files, Archive.VOLUME_ONE_FILTER); List extractedFiles = new ArrayList(); - + for (File file : archiveFiles) { Archive archive = new Archive(file); try { @@ -1120,32 +1104,32 @@ public class CmdlineOperations implements CmdlineInterface { if (!outputFolder.isAbsolute()) { outputFolder = new File(file.getParentFile(), outputFolder.getPath()); } - + CLILogger.info(String.format("Read archive [%s] to [%s]", file.getName(), outputFolder)); final FileMapper outputMapper = new FileMapper(outputFolder, false); - + final List outputMapping = new ArrayList(); for (File entry : archive.listFiles()) { outputMapping.add(outputMapper.getOutputFile(entry)); } - + final Set selection = new TreeSet(); for (File future : outputMapping) { if (filter == null || filter.accept(future)) { selection.add(future); } } - + // check if there is anything to extract at all if (selection.isEmpty()) { continue; } - + boolean skip = true; for (File future : filter == null || forceExtractAll ? outputMapping : selection) { skip &= future.exists(); } - + if (!skip || conflictAction == ConflictAction.OVERRIDE) { if (filter == null || forceExtractAll) { CLILogger.finest("Extracting files " + outputMapping); @@ -1156,7 +1140,7 @@ public class CmdlineOperations implements CmdlineInterface { CLILogger.finest("Extracting files " + selection); // extract files selected by the given filter archive.extract(outputMapper, new FileFilter() { - + @Override public boolean accept(File entry) { return selection.contains(outputMapper.getOutputFile(entry)); @@ -1171,7 +1155,7 @@ public class CmdlineOperations implements CmdlineInterface { archive.close(); } } - + return extractedFiles; } }