2011-10-29 16:24:01 -04:00
|
|
|
|
|
|
|
package net.sourceforge.filebot.cli;
|
|
|
|
|
|
|
|
|
|
|
|
import static java.lang.String.*;
|
|
|
|
import static java.util.Collections.*;
|
|
|
|
import static net.sourceforge.filebot.MediaTypes.*;
|
|
|
|
import static net.sourceforge.filebot.WebServices.*;
|
|
|
|
import static net.sourceforge.filebot.cli.CLILogging.*;
|
|
|
|
import static net.sourceforge.filebot.hash.VerificationUtilities.*;
|
2011-12-26 13:10:53 -05:00
|
|
|
import static net.sourceforge.filebot.media.MediaDetection.*;
|
2011-10-29 16:24:01 -04:00
|
|
|
import static net.sourceforge.filebot.subtitle.SubtitleUtilities.*;
|
|
|
|
import static net.sourceforge.tuned.FileUtilities.*;
|
|
|
|
|
|
|
|
import java.io.File;
|
2011-11-28 07:47:11 -05:00
|
|
|
import java.io.FileFilter;
|
2011-10-29 16:24:01 -04:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
import java.nio.charset.Charset;
|
2011-12-03 03:09:37 -05:00
|
|
|
import java.util.AbstractMap.SimpleImmutableEntry;
|
2011-10-29 16:24:01 -04:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Collection;
|
|
|
|
import java.util.Collections;
|
2011-11-24 12:27:39 -05:00
|
|
|
import java.util.HashMap;
|
2011-10-29 16:24:01 -04:00
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
import java.util.LinkedHashSet;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.ListIterator;
|
|
|
|
import java.util.Locale;
|
|
|
|
import java.util.Map;
|
2011-12-03 03:09:37 -05:00
|
|
|
import java.util.Map.Entry;
|
2012-02-14 09:16:13 -05:00
|
|
|
import java.util.NoSuchElementException;
|
2011-10-29 16:24:01 -04:00
|
|
|
import java.util.Set;
|
2011-12-30 10:34:02 -05:00
|
|
|
import java.util.SortedSet;
|
2011-10-29 16:24:01 -04:00
|
|
|
import java.util.TreeMap;
|
|
|
|
import java.util.TreeSet;
|
|
|
|
import java.util.concurrent.Callable;
|
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
import java.util.concurrent.Executors;
|
|
|
|
import java.util.concurrent.Future;
|
|
|
|
|
|
|
|
import net.sourceforge.filebot.Analytics;
|
|
|
|
import net.sourceforge.filebot.MediaTypes;
|
|
|
|
import net.sourceforge.filebot.WebServices;
|
|
|
|
import net.sourceforge.filebot.format.ExpressionFormat;
|
|
|
|
import net.sourceforge.filebot.format.MediaBindingBean;
|
|
|
|
import net.sourceforge.filebot.hash.HashType;
|
|
|
|
import net.sourceforge.filebot.hash.VerificationFileReader;
|
|
|
|
import net.sourceforge.filebot.hash.VerificationFileWriter;
|
2012-02-10 11:43:09 -05:00
|
|
|
import net.sourceforge.filebot.media.ReleaseInfo;
|
2011-11-23 05:52:46 -05:00
|
|
|
import net.sourceforge.filebot.similarity.EpisodeMetrics;
|
2011-10-29 16:24:01 -04:00
|
|
|
import net.sourceforge.filebot.similarity.Match;
|
|
|
|
import net.sourceforge.filebot.similarity.Matcher;
|
|
|
|
import net.sourceforge.filebot.similarity.NameSimilarityMetric;
|
|
|
|
import net.sourceforge.filebot.similarity.SeriesNameMatcher;
|
2011-12-30 10:34:02 -05:00
|
|
|
import net.sourceforge.filebot.similarity.SimilarityComparator;
|
2011-10-29 16:24:01 -04:00
|
|
|
import net.sourceforge.filebot.similarity.SimilarityMetric;
|
2011-11-23 05:52:46 -05:00
|
|
|
import net.sourceforge.filebot.similarity.StrictEpisodeMetrics;
|
2011-10-29 16:24:01 -04:00
|
|
|
import net.sourceforge.filebot.subtitle.SubtitleFormat;
|
|
|
|
import net.sourceforge.filebot.ui.Language;
|
|
|
|
import net.sourceforge.filebot.ui.rename.HistorySpooler;
|
|
|
|
import net.sourceforge.filebot.vfs.MemoryFile;
|
|
|
|
import net.sourceforge.filebot.web.Episode;
|
|
|
|
import net.sourceforge.filebot.web.EpisodeFormat;
|
|
|
|
import net.sourceforge.filebot.web.EpisodeListProvider;
|
|
|
|
import net.sourceforge.filebot.web.Movie;
|
|
|
|
import net.sourceforge.filebot.web.MovieIdentificationService;
|
2011-12-30 10:34:02 -05:00
|
|
|
import net.sourceforge.filebot.web.MoviePart;
|
2011-10-29 16:24:01 -04:00
|
|
|
import net.sourceforge.filebot.web.SearchResult;
|
2012-02-13 04:54:57 -05:00
|
|
|
import net.sourceforge.filebot.web.SortOrder;
|
2011-10-29 16:24:01 -04:00
|
|
|
import net.sourceforge.filebot.web.SubtitleDescriptor;
|
|
|
|
import net.sourceforge.filebot.web.SubtitleProvider;
|
|
|
|
import net.sourceforge.filebot.web.VideoHashSubtitleService;
|
|
|
|
|
|
|
|
|
|
|
|
public class CmdlineOperations implements CmdlineInterface {
|
|
|
|
|
|
|
|
@Override
|
2012-02-13 04:54:57 -05:00
|
|
|
public List<File> rename(Collection<File> files, String query, String expression, String db, String sortOrder, String languageName, boolean strict) throws Exception {
|
2011-10-29 16:24:01 -04:00
|
|
|
ExpressionFormat format = (expression != null) ? new ExpressionFormat(expression) : null;
|
|
|
|
Locale locale = getLanguage(languageName).toLocale();
|
|
|
|
|
2011-11-20 13:52:57 -05:00
|
|
|
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
|
|
|
|
if (mediaFiles.isEmpty()) {
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("No media files: " + files);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (getEpisodeListProvider(db) != null) {
|
|
|
|
// tv series mode
|
2012-02-13 04:54:57 -05:00
|
|
|
return renameSeries(files, query, format, getEpisodeListProvider(db), SortOrder.forName(sortOrder), locale, strict);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (getMovieIdentificationService(db) != null) {
|
|
|
|
// movie mode
|
|
|
|
return renameMovie(files, query, format, getMovieIdentificationService(db), locale, strict);
|
|
|
|
}
|
|
|
|
|
|
|
|
// auto-determine mode
|
|
|
|
int sxe = 0; // SxE
|
|
|
|
int cws = 0; // common word sequence
|
2011-11-20 13:52:57 -05:00
|
|
|
double max = mediaFiles.size();
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2012-01-01 22:48:24 -05:00
|
|
|
SeriesNameMatcher nameMatcher = new SeriesNameMatcher(getLenientCollator(locale));
|
2011-11-28 04:16:27 -05:00
|
|
|
Collection<String> cwsList = emptySet();
|
|
|
|
if (max >= 5) {
|
2011-12-12 09:06:26 -05:00
|
|
|
cwsList = nameMatcher.matchAll(mediaFiles.toArray(new File[0]));
|
2011-11-28 04:16:27 -05:00
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-11-20 13:52:57 -05:00
|
|
|
for (File f : mediaFiles) {
|
2011-10-29 16:24:01 -04:00
|
|
|
// count SxE matches
|
2012-02-09 08:50:14 -05:00
|
|
|
if (nameMatcher.matchByEpisodeIdentifier(f.getName()) != null) {
|
2011-10-29 16:24:01 -04:00
|
|
|
sxe++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// count CWS matches
|
|
|
|
for (String base : cwsList) {
|
2011-11-22 08:58:47 -05:00
|
|
|
if (base.equalsIgnoreCase(nameMatcher.matchByFirstCommonWordSequence(base, f.getName()))) {
|
2011-10-29 16:24:01 -04:00
|
|
|
cws++;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-11-28 04:16:27 -05:00
|
|
|
CLILogger.finest(format("Filename pattern: [%.02f] SxE, [%.02f] CWS", sxe / max, cws / max));
|
2011-10-29 16:24:01 -04:00
|
|
|
if (sxe >= (max * 0.65) || cws >= (max * 0.65)) {
|
2012-02-13 04:54:57 -05:00
|
|
|
return renameSeries(files, query, format, getEpisodeListProviders()[0], SortOrder.forName(sortOrder), locale, strict); // use default episode db
|
2011-10-29 16:24:01 -04:00
|
|
|
} else {
|
|
|
|
return renameMovie(files, query, format, getMovieIdentificationServices()[0], locale, strict); // use default movie db
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2012-02-13 04:54:57 -05:00
|
|
|
public List<File> renameSeries(Collection<File> files, String query, ExpressionFormat format, EpisodeListProvider db, SortOrder sortOrder, Locale locale, boolean strict) throws Exception {
|
2011-10-29 16:24:01 -04:00
|
|
|
CLILogger.config(format("Rename episodes using [%s]", db.getName()));
|
|
|
|
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
|
|
|
|
|
|
|
|
// similarity metrics for matching
|
2011-11-24 12:27:39 -05:00
|
|
|
SimilarityMetric[] sequence = strict ? StrictEpisodeMetrics.defaultSequence(false) : EpisodeMetrics.defaultSequence(false);
|
2011-10-29 16:24:01 -04:00
|
|
|
List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
|
2011-12-25 10:47:19 -05:00
|
|
|
|
|
|
|
// auto-determine optimal batch sets
|
2012-01-01 22:48:24 -05:00
|
|
|
for (Entry<Set<File>, Set<String>> sameSeriesGroup : mapSeriesNamesByFiles(mediaFiles, locale).entrySet()) {
|
2011-12-25 10:47:19 -05:00
|
|
|
List<List<File>> batchSets = new ArrayList<List<File>>();
|
|
|
|
|
|
|
|
if (sameSeriesGroup.getValue() != null && sameSeriesGroup.getValue().size() > 0) {
|
|
|
|
// handle series name batch set all at once
|
|
|
|
batchSets.add(new ArrayList<File>(sameSeriesGroup.getKey()));
|
|
|
|
} else {
|
|
|
|
// these files don't seem to belong to any series -> handle folder per folder
|
|
|
|
batchSets.addAll(mapByFolder(sameSeriesGroup.getKey()).values());
|
|
|
|
}
|
|
|
|
|
|
|
|
for (List<File> batch : batchSets) {
|
|
|
|
// auto-detect series name if not given
|
2012-01-01 22:48:24 -05:00
|
|
|
Collection<String> seriesNames = (query == null) ? detectQuery(batch, locale, strict) : singleton(query);
|
2011-12-25 10:47:19 -05:00
|
|
|
|
|
|
|
// fetch episode data
|
2012-02-13 04:54:57 -05:00
|
|
|
Set<Episode> episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict);
|
2011-12-25 10:47:19 -05:00
|
|
|
|
|
|
|
if (episodes.size() > 0) {
|
|
|
|
matches.addAll(matchEpisodes(filter(mediaFiles, VIDEO_FILES), episodes, sequence));
|
|
|
|
matches.addAll(matchEpisodes(filter(mediaFiles, SUBTITLE_FILES), episodes, sequence));
|
|
|
|
} else {
|
|
|
|
CLILogger.warning("Failed to fetch episode data: " + mapByFolder(batch).keySet());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
|
|
|
|
if (matches.isEmpty()) {
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("Unable to match files to episode data");
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// map old files to new paths by applying formatting and validating filenames
|
|
|
|
Map<File, File> renameMap = new LinkedHashMap<File, File>();
|
|
|
|
|
|
|
|
for (Match<File, Episode> match : matches) {
|
|
|
|
File file = match.getValue();
|
|
|
|
Episode episode = match.getCandidate();
|
|
|
|
String newName = (format != null) ? format.format(new MediaBindingBean(episode, file)) : EpisodeFormat.SeasonEpisode.format(episode);
|
|
|
|
File newFile = new File(newName + "." + getExtension(file));
|
|
|
|
|
|
|
|
if (isInvalidFilePath(newFile)) {
|
|
|
|
CLILogger.config("Stripping invalid characters from new name: " + newName);
|
|
|
|
newFile = validateFilePath(newFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
renameMap.put(file, newFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
// rename episodes
|
|
|
|
Analytics.trackEvent("CLI", "Rename", "Episode", renameMap.size());
|
|
|
|
return renameAll(renameMap);
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
private List<Match<File, Episode>> matchEpisodes(Collection<File> files, Collection<Episode> episodes, SimilarityMetric[] sequence) throws Exception {
|
|
|
|
// always use strict fail-fast matcher
|
|
|
|
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
|
|
|
|
List<Match<File, Episode>> matches = matcher.match();
|
|
|
|
|
|
|
|
for (File failedMatch : matcher.remainingValues()) {
|
|
|
|
CLILogger.warning("No matching episode: " + failedMatch.getName());
|
|
|
|
}
|
|
|
|
|
|
|
|
return matches;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2012-02-13 04:54:57 -05:00
|
|
|
private Set<Episode> fetchEpisodeSet(final EpisodeListProvider db, final Collection<String> names, final SortOrder sortOrder, final Locale locale, final boolean strict) throws Exception {
|
2011-10-29 16:24:01 -04:00
|
|
|
List<Callable<List<Episode>>> tasks = new ArrayList<Callable<List<Episode>>>();
|
|
|
|
|
|
|
|
// detect series names and create episode list fetch tasks
|
|
|
|
for (final String query : names) {
|
|
|
|
tasks.add(new Callable<List<Episode>>() {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public List<Episode> call() throws Exception {
|
|
|
|
List<SearchResult> results = db.search(query, locale);
|
|
|
|
|
|
|
|
// select search result
|
|
|
|
if (results.size() > 0) {
|
2011-11-29 03:56:29 -05:00
|
|
|
List<SearchResult> selectedSearchResults = selectSearchResult(query, results, strict);
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-11-29 03:56:29 -05:00
|
|
|
if (selectedSearchResults != null) {
|
|
|
|
List<Episode> episodes = new ArrayList<Episode>();
|
|
|
|
for (SearchResult it : selectedSearchResults) {
|
|
|
|
CLILogger.fine(format("Fetching episode data for [%s]", it.getName()));
|
2012-02-13 04:54:57 -05:00
|
|
|
episodes.addAll(db.getEpisodeList(it, sortOrder, locale));
|
2011-11-29 03:56:29 -05:00
|
|
|
Analytics.trackEvent(db.getName(), "FetchEpisodeList", it.getName());
|
|
|
|
}
|
2011-11-06 00:51:42 -04:00
|
|
|
|
|
|
|
return episodes;
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// fetch episode lists concurrently
|
|
|
|
ExecutorService executor = Executors.newCachedThreadPool();
|
|
|
|
|
|
|
|
try {
|
|
|
|
// merge all episodes
|
|
|
|
Set<Episode> episodes = new LinkedHashSet<Episode>();
|
|
|
|
|
|
|
|
for (Future<List<Episode>> future : executor.invokeAll(tasks)) {
|
|
|
|
try {
|
|
|
|
episodes.addAll(future.get());
|
|
|
|
} catch (Exception e) {
|
|
|
|
CLILogger.finest(e.getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// all background workers have finished
|
|
|
|
return episodes;
|
|
|
|
} finally {
|
|
|
|
// destroy background threads
|
|
|
|
executor.shutdown();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2012-02-10 11:43:09 -05:00
|
|
|
public List<File> renameMovie(Collection<File> files, String query, ExpressionFormat format, MovieIdentificationService service, Locale locale, boolean strict) throws Exception {
|
2011-12-30 10:34:02 -05:00
|
|
|
CLILogger.config(format("Rename movies using [%s]", service.getName()));
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
// handle movie files
|
2012-02-10 11:43:09 -05:00
|
|
|
List<File> movieFiles = filter(files, VIDEO_FILES);
|
2012-02-14 09:16:13 -05:00
|
|
|
List<File> nfoFiles = filter(files, MediaTypes.getDefaultFilter("application/nfo"));
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2012-02-14 09:16:13 -05:00
|
|
|
List<File> orphanedFiles = new ArrayList<File>(filter(files, FILES));
|
|
|
|
orphanedFiles.removeAll(movieFiles);
|
|
|
|
orphanedFiles.removeAll(nfoFiles);
|
2012-02-12 21:11:01 -05:00
|
|
|
|
2012-02-10 11:43:09 -05:00
|
|
|
Map<File, List<File>> derivatesByMovieFile = new HashMap<File, List<File>>();
|
|
|
|
for (File movieFile : movieFiles) {
|
|
|
|
derivatesByMovieFile.put(movieFile, new ArrayList<File>());
|
|
|
|
}
|
2012-02-14 09:16:13 -05:00
|
|
|
for (File file : orphanedFiles) {
|
2012-02-10 11:43:09 -05:00
|
|
|
for (File movieFile : movieFiles) {
|
2012-02-12 21:11:01 -05:00
|
|
|
if (isDerived(file, movieFile)) {
|
2012-02-10 11:43:09 -05:00
|
|
|
derivatesByMovieFile.get(movieFile).add(file);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (List<File> derivates : derivatesByMovieFile.values()) {
|
2012-02-14 09:16:13 -05:00
|
|
|
orphanedFiles.removeAll(derivates);
|
2012-02-10 11:43:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// match movie hashes online
|
2012-02-14 09:16:13 -05:00
|
|
|
final Map<File, Movie> movieByFile = new HashMap<File, Movie>();
|
|
|
|
if (query == null) {
|
|
|
|
if (movieFiles.size() > 0) {
|
|
|
|
try {
|
|
|
|
CLILogger.fine(format("Looking up movie by filehash via [%s]", service.getName()));
|
|
|
|
Map<File, Movie> hashLookup = service.getMovieDescriptors(movieFiles, locale);
|
|
|
|
movieByFile.putAll(hashLookup);
|
|
|
|
Analytics.trackEvent(service.getName(), "HashLookup", "Movie", hashLookup.size()); // number of positive hash lookups
|
|
|
|
} catch (UnsupportedOperationException e) {
|
|
|
|
CLILogger.fine(format("%s: Hash lookup not supported", service.getName()));
|
|
|
|
}
|
2012-01-07 09:43:55 -05:00
|
|
|
}
|
2012-02-14 09:16:13 -05:00
|
|
|
for (File nfo : nfoFiles) {
|
|
|
|
try {
|
|
|
|
movieByFile.put(nfo, grepMovie(nfo, service, locale));
|
|
|
|
} catch (NoSuchElementException e) {
|
|
|
|
CLILogger.warning("Failed to grep IMDbID: " + nfo.getName());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2011-12-30 10:34:02 -05:00
|
|
|
CLILogger.fine(format("Looking up movie by query [%s]", query));
|
|
|
|
Movie result = (Movie) selectSearchResult(query, service.searchMovie(query, locale), strict).get(0);
|
2012-02-10 11:43:09 -05:00
|
|
|
// force all mappings
|
2012-02-14 09:16:13 -05:00
|
|
|
for (File file : files) {
|
2012-02-10 11:43:09 -05:00
|
|
|
movieByFile.put(file, result);
|
|
|
|
}
|
2011-12-30 10:34:02 -05:00
|
|
|
}
|
|
|
|
|
2012-02-14 09:16:13 -05:00
|
|
|
List<File> movieMatchFiles = new ArrayList<File>();
|
|
|
|
movieMatchFiles.addAll(movieFiles);
|
|
|
|
movieMatchFiles.addAll(nfoFiles);
|
|
|
|
movieMatchFiles.addAll(filter(files, new ReleaseInfo().getDiskFolderFilter()));
|
|
|
|
movieMatchFiles.addAll(filter(orphanedFiles, SUBTITLE_FILES)); // run movie detection only on orphaned subtitle files
|
|
|
|
|
|
|
|
// map movies to (possibly multiple) files (in natural order)
|
|
|
|
Map<Movie, SortedSet<File>> filesByMovie = new HashMap<Movie, SortedSet<File>>();
|
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
// map all files by movie
|
2012-02-14 09:16:13 -05:00
|
|
|
for (final File file : movieMatchFiles) {
|
2012-02-10 11:43:09 -05:00
|
|
|
Movie movie = movieByFile.get(file);
|
2011-12-30 10:34:02 -05:00
|
|
|
|
|
|
|
// unknown hash, try via imdb id from nfo file
|
|
|
|
if (movie == null) {
|
2012-02-10 11:43:09 -05:00
|
|
|
CLILogger.fine(format("Auto-detect movie from context: [%s]", file));
|
|
|
|
Collection<Movie> results = detectMovie(file, null, service, locale, strict);
|
2011-12-30 10:34:02 -05:00
|
|
|
movie = (Movie) selectSearchResult(query, results, strict).get(0);
|
|
|
|
|
|
|
|
if (movie != null) {
|
|
|
|
Analytics.trackEvent(service.getName(), "SearchMovie", movie.toString(), 1);
|
2011-12-05 10:38:41 -05:00
|
|
|
}
|
2011-12-30 10:34:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// check if we managed to lookup the movie descriptor
|
|
|
|
if (movie != null) {
|
|
|
|
// get file list for movie
|
|
|
|
SortedSet<File> movieParts = filesByMovie.get(movie);
|
2011-12-05 10:38:41 -05:00
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
if (movieParts == null) {
|
|
|
|
movieParts = new TreeSet<File>();
|
|
|
|
filesByMovie.put(movie, movieParts);
|
2011-12-05 10:38:41 -05:00
|
|
|
}
|
2011-12-30 10:34:02 -05:00
|
|
|
|
2012-02-10 11:43:09 -05:00
|
|
|
movieParts.add(file);
|
2011-12-05 10:38:41 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-02-14 09:16:13 -05:00
|
|
|
// collect all File/MoviePart matches
|
2011-12-30 10:34:02 -05:00
|
|
|
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
for (Entry<Movie, SortedSet<File>> entry : filesByMovie.entrySet()) {
|
2012-02-14 09:16:13 -05:00
|
|
|
for (List<File> fileSet : mapByExtension(entry.getValue()).values()) {
|
|
|
|
// resolve movie parts
|
|
|
|
for (int i = 0; i < fileSet.size(); i++) {
|
|
|
|
Movie moviePart = entry.getKey();
|
|
|
|
if (fileSet.size() > 1) {
|
|
|
|
moviePart = new MoviePart(moviePart, i + 1, fileSet.size());
|
|
|
|
}
|
|
|
|
|
|
|
|
matches.add(new Match<File, Movie>(fileSet.get(i), moviePart));
|
|
|
|
|
|
|
|
// automatically add matches for derivate files
|
|
|
|
List<File> derivates = derivatesByMovieFile.get(fileSet.get(i));
|
|
|
|
if (derivates != null) {
|
|
|
|
for (File derivate : derivates) {
|
|
|
|
matches.add(new Match<File, Movie>(derivate, moviePart));
|
|
|
|
}
|
2012-02-10 11:43:09 -05:00
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
// map old files to new paths by applying formatting and validating filenames
|
|
|
|
Map<File, File> renameMap = new LinkedHashMap<File, File>();
|
|
|
|
|
|
|
|
for (Match<File, ?> match : matches) {
|
|
|
|
File file = match.getValue();
|
|
|
|
Object movie = match.getCandidate();
|
|
|
|
String newName = (format != null) ? format.format(new MediaBindingBean(movie, file)) : movie.toString();
|
|
|
|
File newFile = new File(newName + "." + getExtension(file));
|
|
|
|
|
|
|
|
if (isInvalidFilePath(newFile)) {
|
|
|
|
CLILogger.config("Stripping invalid characters from new path: " + newName);
|
|
|
|
newFile = validateFilePath(newFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
renameMap.put(file, newFile);
|
|
|
|
}
|
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
// rename movies
|
|
|
|
Analytics.trackEvent("CLI", "Rename", "Movie", renameMap.size());
|
|
|
|
return renameAll(renameMap);
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
public List<File> renameAll(Map<File, File> renameMap) throws Exception {
|
2011-11-24 12:27:39 -05:00
|
|
|
// rename files
|
|
|
|
final List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
|
|
|
|
|
|
|
|
try {
|
|
|
|
for (Entry<File, File> it : renameMap.entrySet()) {
|
|
|
|
try {
|
|
|
|
// rename file, throw exception on failure
|
2012-02-10 11:43:09 -05:00
|
|
|
File destination = moveRename(it.getKey(), it.getValue());
|
2011-11-24 12:27:39 -05:00
|
|
|
CLILogger.info(format("Renamed [%s] to [%s]", it.getKey(), it.getValue()));
|
|
|
|
|
|
|
|
// remember successfully renamed matches for history entry and possible revert
|
|
|
|
renameLog.add(new SimpleImmutableEntry<File, File>(it.getKey(), destination));
|
|
|
|
} catch (IOException e) {
|
|
|
|
CLILogger.warning(format("Failed to rename [%s]", it.getKey()));
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
|
|
// could not rename one of the files, revert all changes
|
|
|
|
CLILogger.severe(e.getMessage());
|
|
|
|
|
|
|
|
// revert rename operations in reverse order
|
|
|
|
for (ListIterator<Entry<File, File>> iterator = renameLog.listIterator(renameLog.size()); iterator.hasPrevious();) {
|
|
|
|
Entry<File, File> mapping = iterator.previous();
|
|
|
|
|
|
|
|
// revert rename
|
|
|
|
if (mapping.getValue().renameTo(mapping.getKey())) {
|
|
|
|
// remove reverted rename operation from log
|
|
|
|
CLILogger.info("Reverted filename: " + mapping.getKey());
|
|
|
|
} else {
|
|
|
|
// failed to revert rename operation
|
|
|
|
CLILogger.severe("Failed to revert filename: " + mapping.getValue());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Exception("Renaming failed", e);
|
|
|
|
} finally {
|
|
|
|
if (renameLog.size() > 0) {
|
|
|
|
// update rename history
|
|
|
|
HistorySpooler.getInstance().append(renameMap.entrySet());
|
|
|
|
|
|
|
|
// printer number of renamed files if any
|
|
|
|
CLILogger.fine(format("Renamed %d files", renameLog.size()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// new file names
|
|
|
|
List<File> destinationList = new ArrayList<File>();
|
2011-12-30 10:34:02 -05:00
|
|
|
for (Entry<File, File> it : renameLog) {
|
2011-11-24 12:27:39 -05:00
|
|
|
destinationList.add(it.getValue());
|
2011-12-30 10:34:02 -05:00
|
|
|
}
|
2011-11-24 12:27:39 -05:00
|
|
|
|
|
|
|
return destinationList;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
@Override
|
2011-11-25 13:52:31 -05:00
|
|
|
public List<File> getSubtitles(Collection<File> files, String query, String languageName, String output, String csn, boolean strict) throws Exception {
|
2011-11-24 12:27:39 -05:00
|
|
|
final Language language = getLanguage(languageName);
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
// 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;
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
// try to find subtitles for each video file
|
|
|
|
SubtitleCollector collector = new SubtitleCollector(filter(files, VIDEO_FILES));
|
2011-10-29 16:24:01 -04:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
if (collector.isComplete()) {
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("No video files: " + files);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// lookup subtitles by hash
|
|
|
|
for (VideoHashSubtitleService service : WebServices.getVideoHashSubtitleServices()) {
|
2011-11-24 12:27:39 -05:00
|
|
|
if (collector.isComplete()) {
|
2011-10-29 16:24:01 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
try {
|
2011-11-25 13:52:31 -05:00
|
|
|
CLILogger.fine("Looking up subtitles by filehash via " + service.getName());
|
2011-11-24 12:27:39 -05:00
|
|
|
collector.addAll(service.getName(), lookupSubtitleByHash(service, language, collector.remainingVideos()));
|
2011-11-27 09:39:58 -05:00
|
|
|
} catch (Exception e) {
|
2011-11-24 12:27:39 -05:00
|
|
|
CLILogger.warning(format("Lookup by hash failed: " + e.getMessage()));
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-11-25 13:52:31 -05:00
|
|
|
// lookup subtitles via text search, only perform hash lookup in strict mode
|
|
|
|
if ((query != null || !strict) && !collector.isComplete()) {
|
2011-11-24 12:27:39 -05:00
|
|
|
// auto-detect search query
|
2012-01-01 22:48:24 -05:00
|
|
|
Collection<String> querySet = (query == null) ? detectQuery(filter(files, VIDEO_FILES), language.toLocale(), false) : singleton(query);
|
2011-11-24 12:27:39 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
for (SubtitleProvider service : WebServices.getSubtitleProviders()) {
|
2011-11-24 12:27:39 -05:00
|
|
|
if (collector.isComplete()) {
|
2011-10-29 16:24:01 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2011-11-25 13:52:31 -05:00
|
|
|
CLILogger.fine(format("Searching for %s at [%s]", querySet.toString(), service.getName()));
|
2011-11-24 12:27:39 -05:00
|
|
|
collector.addAll(service.getName(), lookupSubtitleByFileName(service, querySet, language, collector.remainingVideos()));
|
2011-11-27 09:39:58 -05:00
|
|
|
} catch (Exception e) {
|
2011-11-24 12:52:11 -05:00
|
|
|
CLILogger.warning(format("Search for [%s] failed: %s", querySet, e.getMessage()));
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// no subtitles for remaining video files
|
2011-11-24 12:27:39 -05:00
|
|
|
for (File it : collector.remainingVideos()) {
|
|
|
|
CLILogger.warning("No matching subtitles found: " + it);
|
|
|
|
}
|
|
|
|
|
|
|
|
// download subtitles in order
|
|
|
|
Map<File, Callable<File>> downloadQueue = new TreeMap<File, Callable<File>>();
|
|
|
|
for (final Entry<String, Map<File, SubtitleDescriptor>> source : collector.subtitlesBySource().entrySet()) {
|
|
|
|
for (final Entry<File, SubtitleDescriptor> descriptor : source.getValue().entrySet()) {
|
|
|
|
downloadQueue.put(descriptor.getKey(), new Callable<File>() {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public File call() throws Exception {
|
|
|
|
Analytics.trackEvent(source.getKey(), "DownloadSubtitle", descriptor.getValue().getLanguageName(), 1);
|
2011-11-25 13:52:31 -05:00
|
|
|
return downloadSubtitle(descriptor.getValue(), descriptor.getKey(), outputFormat, outputEncoding);
|
2011-11-24 12:27:39 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// parallel download
|
|
|
|
List<File> subtitleFiles = new ArrayList<File>();
|
|
|
|
|
2011-11-25 13:52:31 -05:00
|
|
|
if (downloadQueue.size() > 0) {
|
|
|
|
ExecutorService executor = Executors.newFixedThreadPool(4);
|
|
|
|
|
|
|
|
try {
|
|
|
|
for (Future<File> it : executor.invokeAll(downloadQueue.values())) {
|
|
|
|
subtitleFiles.add(it.get());
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
executor.shutdownNow();
|
2011-11-24 12:27:39 -05:00
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
Analytics.trackEvent("CLI", "Download", "Subtitle", subtitleFiles.size());
|
|
|
|
return subtitleFiles;
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-28 07:47:11 -05:00
|
|
|
public List<File> getMissingSubtitles(Collection<File> files, String query, String languageName, String output, String csn, boolean strict) throws Exception {
|
|
|
|
List<File> videoFiles = filter(filter(files, VIDEO_FILES), new FileFilter() {
|
|
|
|
|
|
|
|
// save time on repeating filesystem calls
|
|
|
|
private final Map<File, File[]> cache = new HashMap<File, File[]>();
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-28 07:47:11 -05:00
|
|
|
@Override
|
|
|
|
public boolean accept(File video) {
|
|
|
|
File[] subtitlesByFolder = cache.get(video.getParentFile());
|
|
|
|
if (subtitlesByFolder == null) {
|
|
|
|
subtitlesByFolder = video.getParentFile().listFiles(SUBTITLE_FILES);
|
|
|
|
cache.put(video.getParentFile(), subtitlesByFolder);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (File subtitle : subtitlesByFolder) {
|
|
|
|
if (isDerived(subtitle, video))
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (videoFiles.isEmpty()) {
|
|
|
|
CLILogger.info("No missing subtitles");
|
|
|
|
return emptyList();
|
|
|
|
}
|
|
|
|
|
|
|
|
CLILogger.finest(format("Missing subtitles for %d video files", videoFiles.size()));
|
|
|
|
return getSubtitles(videoFiles, query, languageName, output, csn, strict);
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-25 13:52:31 -05:00
|
|
|
private File downloadSubtitle(SubtitleDescriptor descriptor, File movieFile, SubtitleFormat outputFormat, Charset outputEncoding) throws Exception {
|
2011-10-29 16:24:01 -04:00
|
|
|
// fetch subtitle archive
|
2011-11-24 12:27:39 -05:00
|
|
|
CLILogger.info(format("Fetching [%s]", descriptor.getPath()));
|
2011-11-25 13:52:31 -05:00
|
|
|
MemoryFile subtitleFile = fetchSubtitle(descriptor);
|
2011-10-29 16:24:01 -04:00
|
|
|
|
|
|
|
// subtitle filename is based on movie filename
|
2011-11-25 13:52:31 -05:00
|
|
|
String base = getName(movieFile);
|
2011-10-29 16:24:01 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2011-11-25 13:52:31 -05:00
|
|
|
File destination = new File(movieFile.getParentFile(), formatSubtitle(base, descriptor.getLanguageName(), ext));
|
2011-10-29 16:24:01 -04:00
|
|
|
CLILogger.config(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName()));
|
|
|
|
|
|
|
|
writeFile(data, destination);
|
|
|
|
return destination;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
private Map<File, SubtitleDescriptor> lookupSubtitleByHash(VideoHashSubtitleService service, Language language, Collection<File> videoFiles) throws Exception {
|
|
|
|
Map<File, SubtitleDescriptor> subtitleByVideo = new HashMap<File, SubtitleDescriptor>(videoFiles.size());
|
|
|
|
|
|
|
|
for (Entry<File, List<SubtitleDescriptor>> it : service.getSubtitleList(videoFiles.toArray(new File[0]), language.getName()).entrySet()) {
|
|
|
|
if (it.getValue() != null && it.getValue().size() > 0) {
|
2011-11-25 13:52:31 -05:00
|
|
|
CLILogger.finest(format("Matched [%s] to [%s] via filehash", it.getKey().getName(), it.getValue().get(0).getName()));
|
2011-11-24 12:27:39 -05:00
|
|
|
subtitleByVideo.put(it.getKey(), it.getValue().get(0));
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
2011-11-24 12:27:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return subtitleByVideo;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
private Map<File, SubtitleDescriptor> lookupSubtitleByFileName(SubtitleProvider service, Collection<String> querySet, Language language, Collection<File> videoFiles) throws Exception {
|
|
|
|
Map<File, SubtitleDescriptor> subtitleByVideo = new HashMap<File, SubtitleDescriptor>();
|
|
|
|
|
2011-11-25 13:52:31 -05:00
|
|
|
// search for subtitles
|
|
|
|
List<SubtitleDescriptor> subtitles = findSubtitles(service, querySet, language.getName());
|
2011-11-24 12:27:39 -05:00
|
|
|
|
2011-11-25 13:52:31 -05:00
|
|
|
// match subtitle files to video files
|
|
|
|
if (subtitles.size() > 0) {
|
2011-11-24 12:27:39 -05:00
|
|
|
// first match everything as best as possible, then filter possibly bad matches
|
|
|
|
Matcher<File, SubtitleDescriptor> matcher = new Matcher<File, SubtitleDescriptor>(videoFiles, subtitles, false, EpisodeMetrics.defaultSequence(true));
|
2011-11-27 09:39:58 -05:00
|
|
|
SimilarityMetric sanity = EpisodeMetrics.verificationMetric();
|
2011-11-24 12:27:39 -05:00
|
|
|
|
|
|
|
for (Match<File, SubtitleDescriptor> it : matcher.match()) {
|
2011-12-05 10:38:41 -05:00
|
|
|
if (sanity.getSimilarity(it.getValue(), it.getCandidate()) >= 0.9) {
|
2011-11-25 13:52:31 -05:00
|
|
|
CLILogger.finest(format("Matched [%s] to [%s] via filename", it.getValue().getName(), it.getCandidate().getName()));
|
2011-11-24 12:27:39 -05:00
|
|
|
subtitleByVideo.put(it.getValue(), it.getCandidate());
|
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
return subtitleByVideo;
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2012-01-01 22:48:24 -05:00
|
|
|
private List<String> detectQuery(Collection<File> mediaFiles, Locale locale, boolean strict) throws Exception {
|
2011-12-05 10:38:41 -05:00
|
|
|
// detect series name by common word sequence
|
2012-01-01 22:48:24 -05:00
|
|
|
List<String> names = detectSeriesNames(mediaFiles, locale);
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
if (names.isEmpty() || (strict && names.size() > 1)) {
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("Unable to auto-select query: " + names);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
CLILogger.config("Auto-detected query: " + names);
|
|
|
|
return names;
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-12-27 13:57:11 -05:00
|
|
|
public List<SearchResult> findProbableMatches(final String query, Iterable<? extends SearchResult> searchResults) {
|
2011-10-29 16:24:01 -04:00
|
|
|
// auto-select most probable search result
|
|
|
|
Map<String, SearchResult> probableMatches = new TreeMap<String, SearchResult>(String.CASE_INSENSITIVE_ORDER);
|
|
|
|
|
|
|
|
// use name similarity metric
|
2011-11-29 03:56:29 -05:00
|
|
|
final SimilarityMetric metric = new NameSimilarityMetric();
|
2011-10-29 16:24:01 -04:00
|
|
|
|
|
|
|
// find probable matches using name similarity > 0.9
|
|
|
|
for (SearchResult result : searchResults) {
|
2011-12-30 14:31:33 -05:00
|
|
|
float f = (query == null) ? 1 : metric.getSimilarity(query, result.getName());
|
2011-11-29 03:56:29 -05:00
|
|
|
if (f >= 0.9 || (f >= 0.6 && result.getName().toLowerCase().startsWith(query.toLowerCase()))) {
|
2011-11-24 12:27:39 -05:00
|
|
|
if (!probableMatches.containsKey(result.toString())) {
|
|
|
|
probableMatches.put(result.toString(), result);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-11-29 03:56:29 -05:00
|
|
|
// sort results by similarity to query
|
|
|
|
List<SearchResult> results = new ArrayList<SearchResult>(probableMatches.values());
|
2011-12-30 14:31:33 -05:00
|
|
|
if (query != null) {
|
|
|
|
sort(results, new SimilarityComparator(query));
|
|
|
|
}
|
2011-11-29 03:56:29 -05:00
|
|
|
return results;
|
2011-11-24 12:27:39 -05:00
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-12-30 10:34:02 -05:00
|
|
|
public List<SearchResult> selectSearchResult(String query, Iterable<? extends SearchResult> searchResults, boolean strict) throws Exception {
|
2011-11-29 03:56:29 -05:00
|
|
|
List<SearchResult> probableMatches = findProbableMatches(query, searchResults);
|
2011-11-24 12:27:39 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
if (probableMatches.isEmpty() || (strict && probableMatches.size() != 1)) {
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("Failed to auto-select search result: " + probableMatches);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// return first and only value
|
2011-11-29 03:56:29 -05:00
|
|
|
return probableMatches;
|
2011-11-24 12:27:39 -05:00
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-28 04:16:27 -05:00
|
|
|
private Language getLanguage(String lang) throws Exception {
|
2011-11-24 12:27:39 -05:00
|
|
|
// 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
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("Illegal language code: " + lang);
|
2011-11-24 12:27:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return language;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
private class SubtitleCollector {
|
|
|
|
|
|
|
|
private final Map<String, Map<File, SubtitleDescriptor>> collection = new HashMap<String, Map<File, SubtitleDescriptor>>();
|
|
|
|
private final Set<File> remainingVideos = new TreeSet<File>();
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
public SubtitleCollector(Collection<File> videoFiles) {
|
|
|
|
remainingVideos.addAll(videoFiles);
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
public void addAll(String source, Map<File, SubtitleDescriptor> subtitles) {
|
|
|
|
remainingVideos.removeAll(subtitles.keySet());
|
|
|
|
|
|
|
|
Map<File, SubtitleDescriptor> subtitlesBySource = collection.get(source);
|
|
|
|
if (subtitlesBySource == null) {
|
|
|
|
subtitlesBySource = new TreeMap<File, SubtitleDescriptor>();
|
|
|
|
collection.put(source, subtitlesBySource);
|
|
|
|
}
|
|
|
|
|
|
|
|
subtitlesBySource.putAll(subtitles);
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
public Map<String, Map<File, SubtitleDescriptor>> subtitlesBySource() {
|
|
|
|
return collection;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
public Collection<File> remainingVideos() {
|
|
|
|
return remainingVideos;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-24 12:27:39 -05:00
|
|
|
public boolean isComplete() {
|
|
|
|
return remainingVideos.size() == 0;
|
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
@Override
|
|
|
|
public boolean check(Collection<File> 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;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
@Override
|
|
|
|
public File compute(Collection<File> files, String output, String csn) throws Exception {
|
|
|
|
// check common parent for all given files
|
|
|
|
File root = null;
|
|
|
|
for (File it : files) {
|
|
|
|
if (root == null || root.getPath().startsWith(it.getParent()))
|
|
|
|
root = it.getParentFile();
|
|
|
|
|
|
|
|
if (!it.getParent().startsWith(root.getPath()))
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("Paths don't share a common root: " + files);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// create verification file
|
|
|
|
File outputFile;
|
|
|
|
HashType hashType;
|
|
|
|
|
|
|
|
if (output != null && getExtension(output) != null) {
|
|
|
|
// use given filename
|
|
|
|
hashType = getHashTypeByExtension(getExtension(output));
|
|
|
|
outputFile = new File(root, output);
|
|
|
|
} else {
|
|
|
|
// auto-select the filename based on folder and type
|
|
|
|
hashType = (output != null) ? getHashTypeByExtension(output) : HashType.SFV;
|
|
|
|
outputFile = new File(root, root.getName() + "." + hashType.getFilter().extension());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hashType == null) {
|
2011-11-28 04:16:27 -05:00
|
|
|
throw new Exception("Illegal output type: " + output);
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
CLILogger.config("Using output file: " + outputFile);
|
|
|
|
compute(root.getPath(), files, outputFile, hashType, csn);
|
|
|
|
|
|
|
|
return outputFile;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
private boolean check(File verificationFile, File root) throws Exception {
|
|
|
|
HashType type = getHashType(verificationFile);
|
|
|
|
|
|
|
|
// check if type is supported
|
2011-11-28 04:16:27 -05:00
|
|
|
if (type == null) {
|
|
|
|
throw new Exception("Unsupported format: " + verificationFile);
|
|
|
|
}
|
2011-10-29 16:24:01 -04:00
|
|
|
|
|
|
|
// 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<File, String> 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()));
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
|
|
status = false;
|
|
|
|
CLILogger.warning(e.getMessage());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
parser.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
private void compute(String root, Collection<File> 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) {
|
|
|
|
outputFile.deleteOnExit(); // delete only partially written files
|
|
|
|
throw e;
|
|
|
|
} finally {
|
|
|
|
out.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
@Override
|
2012-02-13 04:54:57 -05:00
|
|
|
public List<String> fetchEpisodeList(String query, String expression, String db, String sortOrderName, String languageName) throws Exception {
|
2011-10-29 16:24:01 -04:00
|
|
|
// find series on the web and fetch episode list
|
|
|
|
ExpressionFormat format = (expression != null) ? new ExpressionFormat(expression) : null;
|
|
|
|
EpisodeListProvider service = (db == null) ? TVRage : getEpisodeListProvider(db);
|
2012-02-13 04:54:57 -05:00
|
|
|
SortOrder sortOrder = SortOrder.forName(sortOrderName);
|
2011-10-29 16:24:01 -04:00
|
|
|
Locale locale = getLanguage(languageName).toLocale();
|
|
|
|
|
2011-11-29 03:56:29 -05:00
|
|
|
SearchResult hit = selectSearchResult(query, service.search(query, locale), false).get(0);
|
2011-10-29 16:24:01 -04:00
|
|
|
List<String> episodes = new ArrayList<String>();
|
|
|
|
|
2012-02-13 04:54:57 -05:00
|
|
|
for (Episode it : service.getEpisodeList(hit, sortOrder, locale)) {
|
2011-10-29 16:24:01 -04:00
|
|
|
String name = (format != null) ? format.format(new MediaBindingBean(it, null)) : EpisodeFormat.SeasonEpisode.format(it);
|
|
|
|
episodes.add(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
return episodes;
|
|
|
|
}
|
|
|
|
|
2011-12-03 03:09:37 -05:00
|
|
|
|
2011-11-02 14:19:09 -04:00
|
|
|
@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));
|
|
|
|
}
|
|
|
|
|
2011-10-29 16:24:01 -04:00
|
|
|
}
|