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 ;
2012-03-24 22:50:28 -04:00
import java.util.Iterator ;
2011-10-29 16:24:01 -04:00
import java.util.LinkedHashMap ;
import java.util.LinkedHashSet ;
import java.util.List ;
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 ;
2012-02-26 07:58:16 -05:00
import net.sourceforge.filebot.archive.Archive ;
import net.sourceforge.filebot.archive.FileMapper ;
2012-03-24 22:50:28 -04:00
import net.sourceforge.filebot.format.ExpressionFilter ;
2011-10-29 16:24:01 -04:00
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 ;
2012-03-17 15:02:04 -04:00
import net.sourceforge.filebot.similarity.EpisodeMatcher ;
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 ;
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 ;
2012-02-19 22:29:00 -05:00
import net.sourceforge.filebot.web.MovieFormat ;
2011-10-29 16:24:01 -04:00
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 ;
2012-02-15 07:40:18 -05:00
import net.sourceforge.tuned.FileUtilities.FolderFilter ;
2011-10-29 16:24:01 -04:00
public class CmdlineOperations implements CmdlineInterface {
@Override
2012-03-24 22:50:28 -04:00
public List < File > rename ( Collection < File > files , String 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 ;
ExpressionFilter filter = ( filterExpression ! = null ) ? new ExpressionFilter ( filterExpression ) : null ;
2012-02-26 07:58:16 -05:00
File outputDir = ( output ! = null & & output . length ( ) > 0 ) ? new File ( output ) : null ;
2012-03-07 09:26:47 -05:00
Locale locale = getLanguage ( lang ) . toLocale ( ) ;
RenameAction renameAction = StandardRenameAction . forName ( action ) ;
2012-03-09 00:38:22 -05:00
ConflictAction conflictAction = ConflictAction . forName ( conflict ) ;
2011-10-29 16:24:01 -04:00
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-03-24 22:50:28 -04:00
return renameSeries ( files , renameAction , conflictAction , outputDir , format , getEpisodeListProvider ( db ) , query , SortOrder . forName ( sortOrder ) , filter , locale , strict ) ;
2011-10-29 16:24:01 -04:00
}
if ( getMovieIdentificationService ( db ) ! = null ) {
// movie mode
2012-03-09 00:38:22 -05:00
return renameMovie ( files , renameAction , conflictAction , outputDir , format , getMovieIdentificationService ( db ) , query , locale , strict ) ;
2011-10-29 16:24:01 -04:00
}
// 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-02-22 12:15:23 -05:00
SeriesNameMatcher nameMatcher = new SeriesNameMatcher ( 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-03-24 22:50:28 -04:00
return renameSeries ( files , renameAction , conflictAction , outputDir , format , WebServices . TVRage , query , SortOrder . forName ( sortOrder ) , filter , locale , strict ) ; // use default episode db
2011-10-29 16:24:01 -04:00
} else {
2012-03-09 00:38:22 -05:00
return renameMovie ( files , renameAction , conflictAction , outputDir , format , WebServices . OpenSubtitles , query , locale , strict ) ; // use default movie db
2011-10-29 16:24:01 -04:00
}
}
2011-12-03 03:09:37 -05:00
2012-03-24 22:50:28 -04:00
public List < File > renameSeries ( Collection < File > files , RenameAction renameAction , ConflictAction conflictAction , File outputDir , ExpressionFormat format , EpisodeListProvider db , String query , SortOrder sortOrder ,
ExpressionFilter filter , 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
2012-03-17 15:02:04 -04:00
List < Match < File , Object > > matches = new ArrayList < Match < File , Object > > ( ) ;
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-03-06 04:58:40 -05:00
Collection < String > seriesNames = ( query = = null ) ? detectQuery ( batch , locale ) : singleton ( query ) ;
if ( strict & & seriesNames . size ( ) > 1 ) {
throw new Exception ( " Handling multiple shows requires non-strict matching " ) ;
}
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
2012-03-24 22:50:28 -04:00
// filter episodes
if ( filter ! = null ) {
CLILogger . fine ( String . format ( " Apply Filter: {%s} " , filter . getExpression ( ) ) ) ;
for ( Iterator < Episode > itr = episodes . iterator ( ) ; itr . hasNext ( ) ; ) {
Episode episode = itr . next ( ) ;
if ( filter . matches ( new MediaBindingBean ( episode , null ) ) ) {
CLILogger . finest ( String . format ( " Exclude [%s] " , episode ) ) ;
itr . remove ( ) ;
}
}
}
2011-12-25 10:47:19 -05:00
if ( episodes . size ( ) > 0 ) {
2012-03-17 15:02:04 -04:00
matches . addAll ( matchEpisodes ( filter ( batch , VIDEO_FILES ) , episodes , strict ) ) ;
matches . addAll ( matchEpisodes ( filter ( batch , SUBTITLE_FILES ) , episodes , strict ) ) ;
2011-12-25 10:47:19 -05:00
} else {
2012-02-19 22:29:00 -05:00
CLILogger . warning ( " Failed to fetch episode data: " + seriesNames ) ;
2011-12-25 10:47:19 -05:00
}
}
}
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 > ( ) ;
2012-03-17 15:02:04 -04:00
for ( Match < File , Object > match : matches ) {
2011-10-29 16:24:01 -04:00
File file = match . getValue ( ) ;
2012-03-17 15:02:04 -04:00
Object episode = match . getCandidate ( ) ;
2012-02-19 22:29:00 -05:00
String newName = ( format ! = null ) ? format . format ( new MediaBindingBean ( episode , file ) ) : validateFileName ( EpisodeFormat . SeasonEpisode . format ( episode ) ) ;
2012-02-26 07:58:16 -05:00
File newFile = new File ( outputDir , newName + " . " + getExtension ( file ) ) ;
2011-10-29 16:24:01 -04:00
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 ( ) ) ;
2012-03-09 00:38:22 -05:00
return renameAll ( renameMap , renameAction , conflictAction ) ;
2011-10-29 16:24:01 -04:00
}
2011-12-03 03:09:37 -05:00
2012-03-17 15:02:04 -04:00
private List < Match < File , Object > > matchEpisodes ( Collection < File > files , Collection < Episode > episodes , boolean strict ) throws Exception {
2011-11-24 12:27:39 -05:00
// always use strict fail-fast matcher
2012-03-17 15:02:04 -04:00
EpisodeMatcher matcher = new EpisodeMatcher ( files , episodes , strict ) ;
List < Match < File , Object > > matches = matcher . match ( ) ;
2011-11-24 12:27:39 -05:00
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-03-09 00:38:22 -05:00
public List < File > renameMovie ( Collection < File > files , RenameAction renameAction , ConflictAction conflictAction , File outputDir , ExpressionFormat format , MovieIdentificationService service , String query , 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 {
2012-02-15 07:40:18 -05:00
Movie movie = grepMovie ( nfo , service , locale ) ;
movieByFile . put ( nfo , movie ) ;
// match movie info to movie files that match the nfo file name
SortedSet < File > siblingMovieFiles = new TreeSet < File > ( filter ( movieFiles , new FolderFilter ( nfo . getParentFile ( ) ) ) ) ;
2012-02-19 22:29:00 -05:00
String baseName = stripReleaseInfo ( getName ( nfo ) ) ;
2012-02-15 07:40:18 -05:00
for ( File movieFile : siblingMovieFiles ) {
2012-02-19 22:29:00 -05:00
if ( baseName . equalsIgnoreCase ( stripReleaseInfo ( getName ( movieFile ) ) ) ) {
2012-02-15 07:40:18 -05:00
movieByFile . put ( movieFile , movie ) ;
}
}
2012-02-14 09:16:13 -05:00
} 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-15 07:40:18 -05:00
// collect files that will be matched one by one
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
2012-02-19 22:29:00 -05:00
// 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 ( ) ) ;
}
2012-02-19 22:29:00 -05:00
matches . add ( new Match < File , Movie > ( fileSet . get ( i ) , moviePart . clone ( ) ) ) ;
2012-02-14 09:16:13 -05:00
// automatically add matches for derivate files
List < File > derivates = derivatesByMovieFile . get ( fileSet . get ( i ) ) ;
if ( derivates ! = null ) {
for ( File derivate : derivates ) {
2012-02-19 22:29:00 -05:00
matches . add ( new Match < File , Movie > ( derivate , moviePart . clone ( ) ) ) ;
2012-02-14 09:16:13 -05:00
}
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 ( ) ;
2012-02-19 22:29:00 -05:00
String newName = ( format ! = null ) ? format . format ( new MediaBindingBean ( movie , file ) ) : validateFileName ( MovieFormat . NameYear . format ( movie ) ) ;
2012-03-02 02:08:54 -05:00
File newFile = new File ( outputDir , newName + " . " + getExtension ( file ) ) ;
2011-12-30 10:34:02 -05:00
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 ( ) ) ;
2012-03-09 00:38:22 -05:00
return renameAll ( renameMap , renameAction , conflictAction ) ;
2011-10-29 16:24:01 -04:00
}
2011-12-03 03:09:37 -05:00
2012-03-09 00:38:22 -05:00
public List < File > renameAll ( Map < File , File > renameMap , RenameAction renameAction , ConflictAction conflictAction ) 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 {
2012-03-09 00:38:22 -05:00
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 ) ;
}
}
}
2011-11-24 12:27:39 -05:00
// rename file, throw exception on failure
2012-03-09 00:38:22 -05:00
if ( ! destination . equals ( source ) & & ! destination . exists ( ) ) {
destination = renameAction . rename ( source , destination ) ;
CLILogger . info ( format ( " [%s] Renamed [%s] to [%s] " , renameAction , it . getKey ( ) , it . getValue ( ) ) ) ;
} else {
CLILogger . info ( format ( " Skipped [%s] because [%s] already exists " , source , destination ) ) ;
}
2011-11-24 12:27:39 -05:00
// remember successfully renamed matches for history entry and possible revert
2012-03-09 00:38:22 -05:00
renameLog . add ( new SimpleImmutableEntry < File , File > ( source , destination ) ) ;
2011-11-24 12:27:39 -05:00
} catch ( IOException e ) {
2012-03-07 09:26:47 -05:00
CLILogger . warning ( format ( " [%s] Failed to rename [%s] " , renameAction , it . getKey ( ) ) ) ;
2011-11-24 12:27:39 -05:00
throw 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-03-06 04:58:40 -05:00
Collection < String > querySet = ( query = = null ) ? detectQuery ( filter ( files , VIDEO_FILES ) , language . toLocale ( ) ) : 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-03-06 04:58:40 -05:00
private List < String > detectQuery ( Collection < File > mediaFiles , Locale locale ) 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
2012-03-06 04:58:40 -05:00
if ( names . isEmpty ( ) ) {
throw new Exception ( " Failed to auto-detect query " ) ;
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
2012-02-19 22:29:00 -05:00
public List < SearchResult > findProbableMatches ( final String query , Iterable < ? extends SearchResult > searchResults , boolean strict ) {
2011-10-29 16:24:01 -04:00
// auto-select most probable search result
2012-03-02 02:08:54 -05:00
Map < String , SearchResult > probableMatches = new LinkedHashMap < String , SearchResult > ( ) ;
2011-10-29 16:24:01 -04:00
// 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
2012-02-19 22:29:00 -05:00
// find probable matches using name similarity > 0.9 (or > 0.8 in non-strict mode)
2011-10-29 16:24:01 -04:00
for ( SearchResult result : searchResults ) {
2011-12-30 14:31:33 -05:00
float f = ( query = = null ) ? 1 : metric . getSimilarity ( query , result . getName ( ) ) ;
2012-02-19 22:29:00 -05:00
if ( f > = ( strict ? 0 . 9 : 0 . 8 ) | | ( f > = 0 . 6 & & result . getName ( ) . toLowerCase ( ) . startsWith ( query . toLowerCase ( ) ) ) ) {
2012-03-02 02:08:54 -05:00
if ( ! probableMatches . containsKey ( result . toString ( ) . toLowerCase ( ) ) ) {
probableMatches . put ( result . toString ( ) . toLowerCase ( ) , 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 {
2012-02-19 22:29:00 -05:00
List < SearchResult > probableMatches = findProbableMatches ( query , searchResults , strict ) ;
2011-11-24 12:27:39 -05:00
2011-10-29 16:24:01 -04:00
if ( probableMatches . isEmpty ( ) | | ( strict & & probableMatches . size ( ) ! = 1 ) ) {
2012-02-20 00:53:36 -05:00
throw new Exception ( " Failed to auto-select search result: " + searchResults ) ;
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 ) ) ;
}
2012-02-26 07:58:16 -05:00
@Override
2012-03-10 05:24:35 -05:00
public List < File > extract ( Collection < File > files , String output , String conflict ) throws Exception {
ConflictAction conflictAction = ConflictAction . forName ( conflict ) ;
2012-02-26 07:58:16 -05:00
// only keep single-volume archives or first part of multi-volume archives
List < File > archiveFiles = filter ( files , Archive . VOLUME_ONE_FILTER ) ;
List < File > extractedFiles = new ArrayList < File > ( ) ;
for ( File file : archiveFiles ) {
Archive archive = new Archive ( file ) ;
2012-02-26 11:57:00 -05:00
try {
File outputFolder = ( output ! = null ) ? new File ( output ) . getAbsoluteFile ( ) : new File ( file . getParentFile ( ) , getNameWithoutExtension ( file . getName ( ) ) ) ;
CLILogger . info ( String . format ( " Extract archive [%s] to [%s] " , file . getName ( ) , outputFolder ) ) ;
FileMapper outputMapper = new FileMapper ( outputFolder , false ) ;
List < File > entries = archive . listFiles ( ) ;
2012-03-10 05:24:35 -05:00
boolean skip = true ;
2012-02-26 11:57:00 -05:00
for ( File entry : entries ) {
2012-03-10 05:24:35 -05:00
File outputFile = outputMapper . getOutputFile ( entry ) ;
skip & = outputFile . exists ( ) ;
extractedFiles . add ( outputFile ) ;
2012-02-26 11:57:00 -05:00
}
2012-03-10 05:24:35 -05:00
if ( ! skip | | conflictAction = = ConflictAction . OVERRIDE ) {
CLILogger . finest ( " Extracting files " + entries ) ;
archive . extract ( outputMapper ) ;
} else {
CLILogger . finest ( " Skipped extracting files " + entries ) ;
}
2012-02-26 11:57:00 -05:00
} finally {
archive . close ( ) ;
2012-02-26 07:58:16 -05:00
}
}
return extractedFiles ;
}
2011-10-29 16:24:01 -04:00
}