2014-04-19 02:30:29 -04:00
package net.filebot.cli ;
2011-10-29 16:24:01 -04:00
2016-03-08 12:02:53 -05:00
import static java.nio.charset.StandardCharsets.* ;
2017-04-18 03:25:34 -04:00
import static java.util.Arrays.* ;
2013-09-11 13:22:00 -04:00
import static java.util.Collections.* ;
2016-03-10 13:32:11 -05:00
import static java.util.stream.Collectors.* ;
2016-03-02 10:02:44 -05:00
import static net.filebot.Logging.* ;
2014-04-19 02:30:29 -04:00
import static net.filebot.MediaTypes.* ;
import static net.filebot.Settings.* ;
import static net.filebot.WebServices.* ;
import static net.filebot.hash.VerificationUtilities.* ;
import static net.filebot.media.MediaDetection.* ;
2016-03-27 09:52:59 -04:00
import static net.filebot.media.XattrMetaInfo.* ;
2014-04-19 02:30:29 -04:00
import static net.filebot.subtitle.SubtitleUtilities.* ;
import static net.filebot.util.FileUtilities.* ;
2011-10-29 16:24:01 -04:00
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 ;
2013-01-27 03:17:12 -05:00
import java.util.AbstractMap ;
2011-10-29 16:24:01 -04:00
import java.util.ArrayList ;
import java.util.Collection ;
2011-11-24 12:27:39 -05:00
import java.util.HashMap ;
2016-03-10 13:32:11 -05:00
import java.util.HashSet ;
2014-04-21 10:05:24 -04:00
import java.util.LinkedHashMap ;
2011-10-29 16:24:01 -04:00
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 ;
2016-03-10 13:32:11 -05:00
import java.util.Objects ;
2011-10-29 16:24:01 -04:00
import java.util.Set ;
2011-12-30 10:34:02 -05:00
import java.util.SortedSet ;
2012-07-18 05:14:58 -04:00
import java.util.TreeMap ;
2011-10-29 16:24:01 -04:00
import java.util.TreeSet ;
2012-04-12 21:56:22 -04:00
import java.util.logging.Level ;
2016-02-03 13:14:14 -05:00
import java.util.stream.IntStream ;
2016-03-10 23:11:40 -05:00
import java.util.stream.Stream ;
2011-10-29 16:24:01 -04:00
2019-02-18 06:13:19 -05:00
import net.filebot.CacheManager ;
2014-04-19 02:30:29 -04:00
import net.filebot.HistorySpooler ;
import net.filebot.Language ;
import net.filebot.RenameAction ;
2015-01-10 16:01:28 -05:00
import net.filebot.StandardRenameAction ;
2014-04-19 02:30:29 -04:00
import net.filebot.archive.Archive ;
import net.filebot.archive.FileMapper ;
2017-02-27 09:11:59 -05:00
import net.filebot.format.ExpressionFileFormat ;
2014-04-19 02:30:29 -04:00
import net.filebot.format.ExpressionFilter ;
import net.filebot.format.ExpressionFormat ;
2019-05-27 03:36:25 -04:00
import net.filebot.format.ExpressionMapper ;
2014-04-19 02:30:29 -04:00
import net.filebot.format.MediaBindingBean ;
import net.filebot.hash.HashType ;
import net.filebot.hash.VerificationFileReader ;
import net.filebot.hash.VerificationFileWriter ;
2016-04-07 10:30:05 -04:00
import net.filebot.media.AutoDetection ;
import net.filebot.media.AutoDetection.Group ;
import net.filebot.media.AutoDetection.Type ;
2018-08-03 08:22:42 -04:00
import net.filebot.media.LocalDatasource ;
2016-04-17 04:44:03 -04:00
import net.filebot.media.VideoQuality ;
2014-04-19 02:30:29 -04:00
import net.filebot.similarity.CommonSequenceMatcher ;
import net.filebot.similarity.EpisodeMatcher ;
import net.filebot.similarity.Match ;
import net.filebot.subtitle.SubtitleFormat ;
import net.filebot.subtitle.SubtitleNaming ;
2015-05-26 12:25:47 -04:00
import net.filebot.util.EntryList ;
2014-04-19 02:30:29 -04:00
import net.filebot.util.FileUtilities.ParentFilter ;
import net.filebot.vfs.FileInfo ;
import net.filebot.vfs.MemoryFile ;
import net.filebot.vfs.SimpleFileInfo ;
import net.filebot.web.AudioTrack ;
2016-04-30 10:59:51 -04:00
import net.filebot.web.Datasource ;
2014-04-19 02:30:29 -04:00
import net.filebot.web.Episode ;
import net.filebot.web.EpisodeListProvider ;
2019-06-10 03:07:09 -04:00
import net.filebot.web.MappedEpisode ;
2014-04-19 02:30:29 -04:00
import net.filebot.web.Movie ;
import net.filebot.web.MovieIdentificationService ;
import net.filebot.web.MoviePart ;
import net.filebot.web.MusicIdentificationService ;
2014-10-28 13:22:48 -04:00
import net.filebot.web.OpenSubtitlesClient ;
2014-04-19 02:30:29 -04:00
import net.filebot.web.SearchResult ;
import net.filebot.web.SortOrder ;
import net.filebot.web.SubtitleDescriptor ;
import net.filebot.web.SubtitleProvider ;
import net.filebot.web.VideoHashSubtitleService ;
2011-10-29 16:24:01 -04:00
public class CmdlineOperations implements CmdlineInterface {
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
@Override
2019-05-27 03:36:25 -04:00
public List < File > rename ( Collection < File > files , Datasource db , String query , SortOrder order , Locale locale , ExpressionFilter filter , ExpressionMapper mapper , boolean strict , ExpressionFileFormat format , File output , RenameAction action , ConflictAction conflict , ExecCommand exec ) throws Exception {
2016-11-27 17:10:42 -05:00
// movie mode
if ( db instanceof MovieIdentificationService ) {
2017-04-18 03:25:34 -04:00
return renameMovie ( files , action , conflict , output , format , ( MovieIdentificationService ) db , query , filter , locale , strict , exec ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
// series mode
if ( db instanceof EpisodeListProvider ) {
2019-05-27 04:25:22 -04:00
return renameSeries ( files , action , conflict , output , format , ( EpisodeListProvider ) db , query , order , filter , mapper , locale , strict , exec ) ;
2016-03-26 15:44:44 -04:00
}
2016-11-27 17:10:42 -05:00
// music mode
if ( db instanceof MusicIdentificationService ) {
2017-04-18 03:25:34 -04:00
return renameMusic ( files , action , conflict , output , format , singletonList ( ( MusicIdentificationService ) db ) , exec ) ;
2013-01-10 13:28:46 -05:00
}
2013-09-11 11:52:35 -04:00
2018-08-03 08:22:42 -04:00
// photo / xattr / plain file mode
if ( db instanceof LocalDatasource ) {
return renameFiles ( files , action , conflict , output , format , ( LocalDatasource ) db , filter , strict , exec ) ;
2014-07-17 03:08:23 -04:00
}
2016-04-07 10:30:05 -04:00
// auto-detect mode for each fileset
AutoDetection auto = new AutoDetection ( files , false , locale ) ;
List < File > results = new ArrayList < File > ( ) ;
for ( Entry < Group , Set < File > > it : auto . group ( ) . entrySet ( ) ) {
2016-04-09 15:16:30 -04:00
if ( it . getKey ( ) . types ( ) . length = = 1 ) {
for ( Type key : it . getKey ( ) . types ( ) ) {
2016-04-07 10:30:05 -04:00
switch ( key ) {
case Movie :
2017-04-18 03:25:34 -04:00
results . addAll ( renameMovie ( it . getValue ( ) , action , conflict , output , format , TheMovieDB , query , filter , locale , strict , exec ) ) ;
2016-04-07 10:30:05 -04:00
break ;
case Series :
2019-05-27 04:25:22 -04:00
results . addAll ( renameSeries ( it . getValue ( ) , action , conflict , output , format , TheTVDB , query , order , filter , mapper , locale , strict , exec ) ) ;
2016-04-07 10:30:05 -04:00
break ;
case Anime :
2019-05-27 04:25:22 -04:00
results . addAll ( renameSeries ( it . getValue ( ) , action , conflict , output , format , TheTVDB , query , SortOrder . Absolute , filter , mapper , locale , strict , exec ) ) ;
2016-04-07 10:30:05 -04:00
break ;
case Music :
2017-04-18 03:25:34 -04:00
results . addAll ( renameMusic ( it . getValue ( ) , action , conflict , output , format , asList ( MediaInfoID3 , AcoustID ) , exec ) ) ; // prefer existing ID3 tags and use acoustid only when necessary
2016-04-07 10:30:05 -04:00
break ;
}
2011-10-29 16:24:01 -04:00
}
2016-04-07 10:30:05 -04:00
} else {
debug . warning ( format ( " Failed to process group: %s => %s " , it . getKey ( ) , it . getValue ( ) ) ) ;
2011-10-29 16:24:01 -04:00
}
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
if ( results . isEmpty ( ) ) {
throw new CmdlineException ( " Failed to identify or process any files " ) ;
}
2016-04-07 10:30:05 -04:00
return results ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2017-03-22 21:46:41 -04:00
@Override
2019-05-27 03:36:25 -04:00
public List < File > renameLinear ( List < File > files , EpisodeListProvider db , String query , SortOrder order , Locale locale , ExpressionFilter filter , ExpressionMapper mapper , ExpressionFileFormat format , File output , RenameAction action , ConflictAction conflict , ExecCommand exec ) throws Exception {
2017-03-22 21:46:41 -04:00
// match files and episodes in linear order
2019-05-27 04:25:22 -04:00
List < Episode > episodes = fetchEpisodeList ( db , query , filter , mapper , order , locale , false ) ;
2017-03-22 21:46:41 -04:00
List < Match < File , ? > > matches = new ArrayList < Match < File , ? > > ( ) ;
for ( int i = 0 ; i < files . size ( ) & & i < episodes . size ( ) ; i + + ) {
matches . add ( new Match < File , Episode > ( files . get ( i ) , episodes . get ( i ) ) ) ;
}
// rename episodes
2019-05-27 03:36:25 -04:00
return renameAll ( formatMatches ( matches , format , output ) , action , conflict , matches , exec ) ;
2017-03-22 21:46:41 -04:00
}
2014-04-20 09:09:01 -04:00
@Override
2016-11-27 17:10:42 -05:00
public List < File > rename ( Map < File , File > renameMap , RenameAction renameAction , ConflictAction conflict ) throws Exception {
2014-04-20 09:09:01 -04:00
// generic rename function that can be passed any set of files
2017-04-18 03:25:34 -04:00
return renameAll ( renameMap , renameAction , conflict , null , null ) ;
2014-04-20 09:09:01 -04:00
}
2019-05-27 04:25:22 -04:00
public List < File > renameSeries ( Collection < File > files , RenameAction renameAction , ConflictAction conflictAction , File outputDir , ExpressionFileFormat format , EpisodeListProvider db , String query , SortOrder sortOrder , ExpressionFilter filter , ExpressionMapper mapper , Locale locale , boolean strict , ExecCommand exec ) throws Exception {
2019-05-22 08:38:26 -04:00
log . config ( format ( " Rename episodes using [%s] with [%s] " , db . getName ( ) , db . vetoRequestParameter ( sortOrder ) ) ) ;
2013-09-11 11:52:35 -04:00
2013-11-28 12:36:27 -05:00
// ignore sample files
2014-04-10 01:55:01 -04:00
List < File > fileset = sortByUniquePath ( filter ( files , not ( getClutterFileFilter ( ) ) ) ) ;
2013-11-28 12:36:27 -05:00
List < File > mediaFiles = filter ( fileset , VIDEO_FILES , SUBTITLE_FILES ) ;
2012-07-26 01:50:47 -04:00
if ( mediaFiles . isEmpty ( ) ) {
2014-07-17 03:08:23 -04:00
throw new CmdlineException ( " No media files: " + files ) ;
2012-07-26 01:50:47 -04:00
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// similarity metrics for matching
2013-01-27 03:17:12 -05:00
List < Match < File , ? > > matches = new ArrayList < Match < File , ? > > ( ) ;
2013-09-11 11:52:35 -04:00
2011-12-25 10:47:19 -05:00
// auto-determine optimal batch sets
2016-04-05 14:06:23 -04:00
for ( Entry < Set < File > , Set < String > > sameSeriesGroup : mapSeriesNamesByFiles ( mediaFiles , locale , db = = AniDB ) . entrySet ( ) ) {
2011-12-25 10:47:19 -05:00
List < List < File > > batchSets = new ArrayList < List < File > > ( ) ;
2013-09-11 11:52:35 -04:00
2011-12-25 10:47:19 -05:00
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 ( ) ) ;
}
2013-09-11 11:52:35 -04:00
2011-12-25 10:47:19 -05:00
for ( List < File > batch : batchSets ) {
2017-02-04 13:07:35 -05:00
// fetch episode data
List < Episode > episodes ;
2013-10-30 01:56:12 -04:00
if ( query = = null ) {
2017-02-04 13:07:35 -05:00
Collection < String > seriesNames = detectSeriesNames ( batch , db = = AniDB , locale ) ; // detect series name by common word sequence
2016-03-02 10:02:44 -05:00
log . config ( " Auto-detected query: " + seriesNames ) ;
2013-09-11 11:52:35 -04:00
2017-02-04 13:07:35 -05:00
if ( seriesNames . size ( ) = = 0 ) {
log . warning ( " Failed to detect query for files: " + batch ) ;
continue ;
}
2013-09-11 11:52:35 -04:00
2017-02-04 13:07:35 -05:00
if ( strict & & seriesNames . size ( ) > 1 ) {
throw new CmdlineException ( " Multiple queries: Processing multiple shows at once requires -non-strict matching: " + seriesNames ) ;
}
episodes = fetchEpisodeSet ( db , seriesNames , sortOrder , locale , strict , 5 ) ; // consider episodes of up to N search results for each query
} else {
2018-07-08 16:15:26 -04:00
if ( isSeriesID ( query ) ) {
episodes = db . getEpisodeList ( Integer . parseInt ( query ) , sortOrder , locale ) ;
} else {
episodes = fetchEpisodeSet ( db , singleton ( query ) , sortOrder , locale , false , 1 ) ; // use --q option and pick first result
}
2013-10-30 01:56:12 -04:00
}
2017-02-04 13:07:35 -05:00
if ( episodes . isEmpty ( ) ) {
2012-03-25 21:18:27 -04:00
continue ;
}
2013-09-11 11:52:35 -04:00
2019-05-27 04:25:22 -04:00
// filter episodes and apply custom mappings
2013-09-11 11:52:35 -04:00
episodes = applyExpressionFilter ( episodes , filter ) ;
2019-06-10 03:07:09 -04:00
episodes = applyEpisodeExpressionMapper ( episodes , mapper ) ;
2013-09-11 11:52:35 -04:00
2014-03-16 13:46:30 -04:00
for ( List < File > filesPerType : mapByMediaExtension ( filter ( batch , VIDEO_FILES , SUBTITLE_FILES ) ) . values ( ) ) {
2019-06-10 03:33:07 -04:00
matchEpisodes ( filesPerType , episodes , strict ) . stream ( ) . map ( this : : unmap ) . forEach ( matches : : add ) ;
2014-03-16 13:46:30 -04:00
}
2011-12-25 10:47:19 -05:00
}
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
if ( matches . isEmpty ( ) ) {
2016-04-07 10:30:05 -04:00
throw new CmdlineException ( " Failed to match files to episode data " ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2013-04-14 00:29:41 -04:00
// handle derived files
List < Match < File , ? > > derivateMatches = new ArrayList < Match < File , ? > > ( ) ;
2013-11-28 12:36:27 -05:00
SortedSet < File > derivateFiles = new TreeSet < File > ( fileset ) ;
2013-04-14 00:29:41 -04:00
derivateFiles . removeAll ( mediaFiles ) ;
2013-09-11 11:52:35 -04:00
2013-04-14 00:29:41 -04:00
for ( File file : derivateFiles ) {
for ( Match < File , ? > match : matches ) {
2015-01-18 07:47:57 -05:00
if ( file . getPath ( ) . startsWith ( match . getValue ( ) . getParentFile ( ) . getPath ( ) ) & & isDerived ( file , match . getValue ( ) ) & & match . getCandidate ( ) instanceof Episode ) {
2013-04-14 00:29:41 -04:00
derivateMatches . add ( new Match < File , Object > ( file , ( ( Episode ) match . getCandidate ( ) ) . clone ( ) ) ) ;
break ;
}
}
}
2013-09-11 11:52:35 -04:00
2013-04-14 00:29:41 -04:00
// add matches from other files that are linked via filenames
matches . addAll ( derivateMatches ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// rename episodes
2017-04-18 03:25:34 -04:00
return renameAll ( formatMatches ( matches , format , outputDir ) , renameAction , conflictAction , matches , exec ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2019-06-10 03:33:07 -04:00
private Match < File , ? > unmap ( Match < File , ? > match ) {
// add matches and unmap mapped episodes
if ( match . getCandidate ( ) instanceof MappedEpisode ) {
MappedEpisode mapping = ( MappedEpisode ) match . getCandidate ( ) ;
log . fine ( format ( " Reverse Map [%s] to [%s] " , mapping . getMapping ( ) , mapping . getOriginal ( ) ) ) ;
return new Match < File , Episode > ( match . getValue ( ) , mapping . getOriginal ( ) ) ;
}
return match ;
}
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 ( ) ;
2013-09-11 11:52:35 -04:00
2011-11-24 12:27:39 -05:00
for ( File failedMatch : matcher . remainingValues ( ) ) {
2016-03-02 10:02:44 -05:00
log . warning ( " No matching episode: " + failedMatch . getName ( ) ) ;
2011-11-24 12:27:39 -05:00
}
2013-09-11 11:52:35 -04:00
2014-08-13 12:23:02 -04:00
// in non-strict mode just pass back results as we got it from the matcher
if ( ! strict ) {
return matches ;
}
// in strict mode sanity check the result and only pass back good matches
List < Match < File , Object > > validMatches = new ArrayList < Match < File , Object > > ( ) ;
for ( Match < File , Object > it : matches ) {
2014-08-13 14:07:21 -04:00
if ( isEpisodeNumberMatch ( it . getValue ( ) , ( Episode ) it . getCandidate ( ) ) ) {
2014-08-13 12:23:02 -04:00
validMatches . add ( it ) ;
}
}
return validMatches ;
2011-11-24 12:27:39 -05:00
}
2013-09-11 11:52:35 -04:00
2017-02-04 13:07:35 -05:00
private List < Episode > fetchEpisodeSet ( EpisodeListProvider db , Collection < String > names , SortOrder sortOrder , Locale locale , boolean strict , int limit ) throws Exception {
2013-03-15 15:53:09 -04:00
Set < SearchResult > shows = new LinkedHashSet < SearchResult > ( ) ;
Set < Episode > episodes = new LinkedHashSet < Episode > ( ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// detect series names and create episode list fetch tasks
2013-03-15 15:53:09 -04:00
for ( String query : names ) {
List < SearchResult > results = db . search ( query , locale ) ;
2013-09-11 11:52:35 -04:00
2013-03-15 15:53:09 -04:00
// select search result
if ( results . size ( ) > 0 ) {
2017-02-05 11:26:24 -05:00
List < SearchResult > selectedSearchResults = selectSearchResult ( query , results , true , true , strict , limit ) ;
2013-09-11 11:52:35 -04:00
2013-03-15 15:53:09 -04:00
if ( selectedSearchResults ! = null ) {
for ( SearchResult it : selectedSearchResults ) {
if ( shows . add ( it ) ) {
try {
2016-03-02 10:02:44 -05:00
log . fine ( format ( " Fetching episode data for [%s] " , it . getName ( ) ) ) ;
2013-03-15 15:53:09 -04:00
episodes . addAll ( db . getEpisodeList ( it , sortOrder , locale ) ) ;
} catch ( IOException e ) {
2016-03-02 10:02:44 -05:00
throw new CmdlineException ( String . format ( " Failed to fetch episode data for [%s]: %s " , it , e . getMessage ( ) ) , e ) ;
2011-11-29 03:56:29 -05:00
}
2011-10-29 16:24:01 -04:00
}
}
}
}
}
2013-09-11 11:52:35 -04:00
2017-02-04 13:07:35 -05:00
if ( episodes . isEmpty ( ) ) {
log . warning ( " Failed to fetch episode data: " + names ) ;
}
2016-03-20 14:33:31 -04:00
return new ArrayList < Episode > ( episodes ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2017-04-18 03:25:34 -04:00
public List < File > renameMovie ( Collection < File > files , RenameAction renameAction , ConflictAction conflictAction , File outputDir , ExpressionFileFormat format , MovieIdentificationService service , String query , ExpressionFilter filter , Locale locale , boolean strict , ExecCommand exec ) throws Exception {
2016-03-02 10:02:44 -05:00
log . config ( format ( " Rename movies using [%s] " , service . getName ( ) ) ) ;
2013-09-11 11:52:35 -04:00
2012-06-15 06:45:35 -04:00
// ignore sample files
2014-03-20 03:45:56 -04:00
List < File > fileset = sortByUniquePath ( filter ( files , not ( getClutterFileFilter ( ) ) ) ) ;
2013-09-11 11:52:35 -04:00
2011-12-30 10:34:02 -05:00
// handle movie files
2012-07-18 05:14:58 -04:00
Set < File > movieFiles = new TreeSet < File > ( filter ( fileset , VIDEO_FILES ) ) ;
2012-07-26 01:50:47 -04:00
Set < File > nfoFiles = new TreeSet < File > ( filter ( fileset , NFO_FILES ) ) ;
2013-09-11 11:52:35 -04:00
2012-06-15 06:45:35 -04:00
List < File > orphanedFiles = new ArrayList < File > ( filter ( fileset , FILES ) ) ;
2012-02-14 09:16:13 -05:00
orphanedFiles . removeAll ( movieFiles ) ;
orphanedFiles . removeAll ( nfoFiles ) ;
2013-09-11 11:52:35 -04: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-06-22 03:47:26 -04:00
List < File > orphanParent = listPath ( file ) ;
2012-02-10 11:43:09 -05:00
for ( File movieFile : movieFiles ) {
2012-06-22 03:47:26 -04:00
if ( orphanParent . contains ( movieFile . getParentFile ( ) ) & & 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
}
2013-09-11 11:52:35 -04:00
2012-02-10 11:43:09 -05:00
// match movie hashes online
2016-08-08 05:05:23 -04:00
Map < File , Movie > movieByFile = new TreeMap < File , Movie > ( ) ;
2012-02-14 09:16:13 -05:00
if ( query = = null ) {
2012-07-18 05:14:58 -04:00
// collect useful nfo files even if they are not part of the selected fileset
Set < File > effectiveNfoFileSet = new TreeSet < File > ( nfoFiles ) ;
for ( File dir : mapByFolder ( movieFiles ) . keySet ( ) ) {
2014-07-28 06:00:27 -04:00
effectiveNfoFileSet . addAll ( getChildren ( dir , NFO_FILES ) ) ;
2012-07-18 05:14:58 -04:00
}
2012-07-28 06:57:50 -04:00
for ( File dir : filter ( fileset , FOLDERS ) ) {
2014-07-28 06:00:27 -04:00
effectiveNfoFileSet . addAll ( getChildren ( dir , NFO_FILES ) ) ;
2012-07-28 06:57:50 -04:00
}
2013-09-11 11:52:35 -04:00
2012-07-18 05:14:58 -04:00
for ( File nfo : effectiveNfoFileSet ) {
2012-02-14 09:16:13 -05:00
try {
2012-02-15 07:40:18 -05:00
Movie movie = grepMovie ( nfo , service , locale ) ;
2013-09-11 11:52:35 -04:00
2012-07-28 06:57:50 -04:00
// ignore illegal nfos
if ( movie = = null ) {
continue ;
}
2013-09-11 11:52:35 -04:00
2012-07-18 05:14:58 -04:00
if ( nfoFiles . contains ( nfo ) ) {
movieByFile . put ( nfo , movie ) ;
}
2013-09-11 11:52:35 -04:00
2012-07-28 06:57:50 -04:00
if ( isDiskFolder ( nfo . getParentFile ( ) ) ) {
// special handling for disk folders
for ( File folder : fileset ) {
if ( nfo . getParentFile ( ) . equals ( folder ) ) {
movieByFile . put ( folder , movie ) ;
}
}
} else {
// match movie info to movie files that match the nfo file name
SortedSet < File > siblingMovieFiles = new TreeSet < File > ( filter ( movieFiles , new ParentFilter ( nfo . getParentFile ( ) ) ) ) ;
String baseName = stripReleaseInfo ( getName ( nfo ) ) . toLowerCase ( ) ;
2013-09-11 11:52:35 -04:00
2012-07-28 06:57:50 -04:00
for ( File movieFile : siblingMovieFiles ) {
2012-07-29 08:42:05 -04:00
if ( ! baseName . isEmpty ( ) & & stripReleaseInfo ( getName ( movieFile ) ) . toLowerCase ( ) . startsWith ( baseName ) ) {
2012-07-28 06:57:50 -04:00
movieByFile . put ( movieFile , movie ) ;
}
2012-02-15 07:40:18 -05:00
}
}
2016-08-02 13:51:53 -04:00
} catch ( Exception e ) {
log . log ( Level . WARNING , " Failed to grep IMDbID: " + nfo . getName ( ) , e ) ;
2012-02-14 09:16:13 -05:00
}
}
} else {
2016-03-02 10:02:44 -05:00
log . fine ( format ( " Looking up movie by query [%s] " , query ) ) ;
2013-09-11 11:52:35 -04:00
List < Movie > results = service . searchMovie ( query , locale ) ;
2016-08-11 07:27:05 -04:00
List < Movie > options = applyExpressionFilter ( results , filter ) ;
if ( options . isEmpty ( ) ) {
2016-04-07 10:30:05 -04:00
throw new CmdlineException ( " Failed to find a valid match: " + results ) ;
2013-09-11 11:52:35 -04:00
}
2012-02-10 11:43:09 -05:00
// force all mappings
2017-02-05 11:26:24 -05:00
Movie movie = selectSearchResult ( query , options ) ;
2012-02-14 09:16:13 -05:00
for ( File file : files ) {
2017-02-04 13:07:35 -05:00
movieByFile . put ( file , movie ) ;
2012-02-10 11:43:09 -05:00
}
2011-12-30 10:34:02 -05:00
}
2013-09-11 11:52:35 -04: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 ) ;
2012-07-26 01:50:47 -04:00
movieMatchFiles . addAll ( filter ( files , FOLDERS ) ) ;
2012-02-14 09:16:13 -05:00
movieMatchFiles . addAll ( filter ( orphanedFiles , SUBTITLE_FILES ) ) ; // run movie detection only on orphaned subtitle files
2013-09-11 11:52:35 -04:00
2012-07-26 01:50:47 -04:00
// sanity check that we have something to do
if ( fileset . isEmpty ( ) | | movieMatchFiles . isEmpty ( ) ) {
2014-07-17 03:08:23 -04:00
throw new CmdlineException ( " No media files: " + files ) ;
2012-07-26 01:50:47 -04:00
}
2013-09-11 11:52:35 -04:00
2012-07-23 12:14:19 -04:00
// map movies to (possibly multiple) files (in natural order)
2012-02-19 22:29:00 -05:00
Map < Movie , SortedSet < File > > filesByMovie = new HashMap < Movie , SortedSet < File > > ( ) ;
2013-09-11 11:52:35 -04:00
2011-12-30 10:34:02 -05:00
// map all files by movie
2016-08-08 05:05:23 -04:00
for ( File file : movieMatchFiles ) {
2012-02-10 11:43:09 -05:00
Movie movie = movieByFile . get ( file ) ;
2013-09-11 11:52:35 -04:00
2011-12-30 10:34:02 -05:00
// unknown hash, try via imdb id from nfo file
if ( movie = = null ) {
2016-03-02 10:02:44 -05:00
log . fine ( format ( " Auto-detect movie from context: [%s] " , file ) ) ;
2017-09-22 02:53:49 -04:00
List < Movie > options = detectMovieWithYear ( file , service , locale , strict ) ;
// ignore files that cannot yield any acceptable matches (e.g. movie files without year in strict mode)
if ( options = = null ) {
continue ;
}
2014-05-06 14:49:41 -04:00
// apply filter if defined
options = applyExpressionFilter ( options , filter ) ;
// reduce options to perfect matches if possible
List < Movie > perfectMatches = matchMovieByWordSequence ( getName ( file ) , options , 0 ) ;
2017-02-05 11:26:24 -05:00
// narrow down options if possible
2014-05-06 14:49:41 -04:00
if ( perfectMatches . size ( ) > 0 ) {
options = perfectMatches ;
}
2012-06-22 03:47:26 -04:00
try {
2014-05-06 14:49:41 -04:00
// select first element if matches are reliable
if ( options . size ( ) > 0 ) {
2017-02-05 11:26:24 -05:00
movie = selectSearchResult ( stripReleaseInfo ( getName ( file ) ) , options ) ;
2016-08-08 05:05:23 -04:00
2014-09-15 16:35:13 -04:00
// make sure to get the language-specific movie object for the selected option
2016-08-08 05:05:23 -04:00
movie = getLocalizedMovie ( service , movie , locale ) ;
2013-09-13 02:56:30 -04:00
}
2012-06-22 03:47:26 -04:00
} catch ( Exception e ) {
2017-02-05 11:26:24 -05:00
log . warning ( cause ( e ) ) ;
2012-06-22 03:47:26 -04:00
}
2011-12-30 10:34:02 -05:00
}
2013-09-11 11:52:35 -04:00
2011-12-30 10:34:02 -05:00
// check if we managed to lookup the movie descriptor
if ( movie ! = null ) {
2017-09-22 02:53:49 -04:00
// add to file list for movie
filesByMovie . computeIfAbsent ( movie , k - > new TreeSet < File > ( ) ) . add ( file ) ;
2011-12-05 10:38:41 -05:00
}
}
2013-09-11 11:52:35 -04: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 , ? > > ( ) ;
2013-09-11 11:52:35 -04:00
2016-08-07 17:22:05 -04:00
filesByMovie . forEach ( ( movie , fs ) - > {
groupByMediaCharacteristics ( fs ) . forEach ( moviePartFiles - > {
// resolve movie parts
for ( int i = 0 ; i < moviePartFiles . size ( ) ; i + + ) {
Movie moviePart = moviePartFiles . size ( ) = = 1 ? movie : new MoviePart ( movie , i + 1 , moviePartFiles . size ( ) ) ;
matches . add ( new Match < File , Movie > ( moviePartFiles . get ( i ) , moviePart . clone ( ) ) ) ;
// automatically add matches for derived files
List < File > derivates = derivatesByMovieFile . get ( moviePartFiles . get ( i ) ) ;
if ( derivates ! = null ) {
for ( File derivate : derivates ) {
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
}
2016-08-07 17:22:05 -04:00
} ) ;
} ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// rename movies
2017-04-18 03:25:34 -04:00
return renameAll ( formatMatches ( matches , format , outputDir ) , renameAction , conflictAction , matches , exec ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2017-04-18 03:25:34 -04:00
public List < File > renameMusic ( Collection < File > files , RenameAction renameAction , ConflictAction conflictAction , File outputDir , ExpressionFileFormat format , List < MusicIdentificationService > services , ExecCommand exec ) throws Exception {
2015-09-29 12:31:28 -04:00
List < File > audioFiles = sortByUniquePath ( filter ( files , AUDIO_FILES , VIDEO_FILES ) ) ;
2013-09-11 11:52:35 -04:00
2016-04-07 10:30:05 -04:00
// check audio files against all services if necessary
2013-01-27 03:17:12 -05:00
List < Match < File , ? > > matches = new ArrayList < Match < File , ? > > ( ) ;
2016-04-07 10:30:05 -04:00
LinkedHashSet < File > remaining = new LinkedHashSet < File > ( audioFiles ) ;
// check audio files against all services
2017-04-18 03:25:34 -04:00
for ( MusicIdentificationService service : services ) {
if ( remaining . size ( ) > 0 ) {
log . config ( format ( " Rename music using %s " , service . getIdentifier ( ) ) ) ;
service . lookup ( remaining ) . forEach ( ( file , music ) - > {
if ( music ! = null ) {
matches . add ( new Match < File , AudioTrack > ( file , music . clone ( ) ) ) ;
remaining . remove ( file ) ;
}
} ) ;
}
2013-01-27 03:17:12 -05:00
}
2013-09-11 11:52:35 -04:00
2013-01-14 13:08:13 -05:00
// error logging
2016-04-07 10:30:05 -04:00
remaining . forEach ( f - > log . warning ( format ( " Failed to process music file: %s " , f ) ) ) ;
2013-09-11 11:52:35 -04:00
2013-01-10 13:28:46 -05:00
// rename movies
2017-04-18 03:25:34 -04:00
return renameAll ( formatMatches ( matches , format , outputDir ) , renameAction , conflictAction , null , exec ) ;
2013-01-10 13:28:46 -05:00
}
2013-09-11 11:52:35 -04:00
2018-08-03 08:22:42 -04:00
public List < File > renameFiles ( Collection < File > files , RenameAction renameAction , ConflictAction conflictAction , File outputDir , ExpressionFileFormat format , LocalDatasource service , ExpressionFilter filter , boolean strict , ExecCommand exec ) throws Exception {
2016-03-02 10:02:44 -05:00
log . config ( format ( " Rename files using [%s] " , service . getName ( ) ) ) ;
2014-07-17 03:08:23 -04:00
2017-03-23 04:53:20 -04:00
Map < File , File > renameMap = new LinkedHashMap < File , File > ( ) ;
2016-10-05 03:00:03 -04:00
// match to xattr metadata object or the file itself
Map < File , Object > matches = service . match ( files , strict ) ;
2017-03-23 04:53:20 -04:00
service . match ( files , strict ) . forEach ( ( k , v ) - > {
MediaBindingBean bindingBean = new MediaBindingBean ( v , k , matches ) ;
2014-07-17 03:08:23 -04:00
if ( filter = = null | | filter . matches ( bindingBean ) ) {
2017-03-23 04:53:20 -04:00
String destinationPath = format ! = null ? format . format ( bindingBean ) : v instanceof File ? v . toString ( ) : validateFileName ( v . toString ( ) ) ;
renameMap . put ( k , getDestinationFile ( k , destinationPath , outputDir ) ) ;
2014-07-17 03:08:23 -04:00
}
2017-03-23 04:53:20 -04:00
} ) ;
2014-07-17 03:08:23 -04:00
2017-04-18 03:25:34 -04:00
return renameAll ( renameMap , renameAction , conflictAction , null , exec ) ;
2014-07-17 03:08:23 -04:00
}
2016-04-07 10:30:05 -04:00
private Map < File , Object > getContext ( List < Match < File , ? > > matches ) {
2013-01-27 03:17:12 -05:00
return new AbstractMap < File , Object > ( ) {
2013-09-11 11:52:35 -04:00
2013-01-27 03:17:12 -05:00
@Override
public Set < Entry < File , Object > > entrySet ( ) {
2017-04-18 03:25:34 -04:00
return matches . stream ( ) . collect ( toMap ( it - > it . getValue ( ) , it - > ( Object ) it . getCandidate ( ) , ( a , b ) - > a , LinkedHashMap : : new ) ) . entrySet ( ) ;
2013-01-27 03:17:12 -05:00
}
} ;
}
2013-09-11 11:52:35 -04:00
2012-07-16 15:08:35 -04:00
private File getDestinationFile ( File original , String newName , File outputDir ) {
2012-07-26 01:50:47 -04:00
String extension = getExtension ( original ) ;
2014-02-23 21:12:33 -05:00
File newFile = new File ( extension ! = null ? newName + '.' + extension . toLowerCase ( ) : newName ) ;
2013-09-11 11:52:35 -04:00
2012-07-16 15:08:35 -04:00
// resolve against output dir
if ( outputDir ! = null & & ! newFile . isAbsolute ( ) ) {
newFile = new File ( outputDir , newFile . getPath ( ) ) ;
}
2013-09-11 11:52:35 -04:00
2012-07-16 15:08:35 -04:00
if ( isInvalidFilePath ( newFile ) & & ! isUnixFS ( ) ) {
2016-03-02 10:02:44 -05:00
log . config ( " Stripping invalid characters from new path: " + newName ) ;
2012-07-16 15:08:35 -04:00
newFile = validateFilePath ( newFile ) ;
}
2013-09-11 11:52:35 -04:00
2012-07-16 15:08:35 -04:00
return newFile ;
}
2013-09-11 11:52:35 -04:00
2017-03-26 14:39:38 -04:00
private Map < File , File > formatMatches ( List < Match < File , ? > > matches , ExpressionFileFormat format , File outputDir ) throws Exception {
2017-03-23 04:53:20 -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 , ? > match : matches ) {
File file = match . getValue ( ) ;
Object object = match . getCandidate ( ) ;
String destinationPath = format ! = null ? format . format ( new MediaBindingBean ( object , file , getContext ( matches ) ) ) : validateFileName ( object . toString ( ) ) ;
renameMap . put ( file , getDestinationFile ( file , destinationPath , outputDir ) ) ;
}
return renameMap ;
}
2017-04-18 03:25:34 -04:00
protected List < File > renameAll ( Map < File , File > renameMap , RenameAction renameAction , ConflictAction conflictAction , List < Match < File , ? > > matches , ExecCommand exec ) throws Exception {
2019-02-18 06:13:19 -05:00
// flush all memory caches to disk (before starting any long running file system operations that might be cancelled by the user)
CacheManager . getInstance ( ) . flushAll ( ) ;
2013-09-13 02:56:30 -04:00
if ( renameMap . isEmpty ( ) ) {
2016-04-07 10:30:05 -04:00
throw new CmdlineException ( " Failed to identify or process any files " ) ;
2013-09-13 02:56:30 -04:00
}
2018-06-23 03:48:10 -04:00
// allow --action test for evaluation purposes
if ( renameAction ! = StandardRenameAction . TEST ) {
LICENSE . check ( ) ;
}
2018-06-10 13:45:42 -04:00
2011-11-24 12:27:39 -05:00
// rename files
2016-02-03 13:14:14 -05:00
Map < File , File > renameLog = new LinkedHashMap < File , File > ( ) ;
2013-09-11 11:52:35 -04:00
2011-11-24 12:27:39 -05:00
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 ( ) ;
2013-09-11 11:52:35 -04:00
2012-03-09 00:38:22 -05:00
// resolve destination
if ( ! destination . isAbsolute ( ) ) {
// same folder, different name
2016-04-13 07:22:03 -04:00
destination = resolve ( source , destination ) ;
2012-03-09 00:38:22 -05:00
}
2013-09-11 11:52:35 -04:00
2018-09-02 00:07:08 -04:00
if ( ! destination . equals ( source ) & & existsNoFollowLinks ( destination ) ) {
2012-03-09 00:38:22 -05:00
if ( conflictAction = = ConflictAction . FAIL ) {
2017-04-15 12:43:25 -04:00
throw new CmdlineException ( String . format ( " Failed to process [%s] because [%s] already exists " , source , destination ) ) ;
2012-03-09 00:38:22 -05:00
}
2013-09-11 11:52:35 -04:00
2017-04-15 12:43:25 -04:00
// do not allow abuse of online databases by repeatedly processing the same files
2018-12-15 01:46:27 -05:00
if ( matches ! = null & & renameAction . canRevert ( ) & & source . length ( ) > 0 & & equalsLastModified ( source , destination , 2000 ) & & equalsFileContent ( source , destination ) ) {
throw new CmdlineException ( String . format ( " Failed to process [%s] because [%s] is an exact copy and already exists [Last-Modified: %tc] " , source , destination , destination . lastModified ( ) ) ) ;
2017-04-15 12:43:25 -04:00
}
// delete existing destination path if necessary
2016-04-17 04:44:03 -04:00
if ( conflictAction = = ConflictAction . OVERRIDE | | ( conflictAction = = ConflictAction . AUTO & & VideoQuality . isBetter ( source , destination ) ) ) {
2019-01-28 09:31:10 -05:00
log . fine ( format ( " [%s] Delete [%s] " , conflictAction , destination ) ) ;
// do not actually delete files in test mode
if ( renameAction ! = StandardRenameAction . TEST ) {
2017-03-24 13:30:01 -04:00
try {
delete ( destination ) ;
} catch ( Exception e ) {
log . warning ( format ( " [%s] Failed to delete [%s]: %s " , conflictAction , destination , e ) ) ;
}
2012-03-09 00:38:22 -05:00
}
2017-04-15 12:43:25 -04:00
}
// generate indexed destination path if necessary
if ( conflictAction = = ConflictAction . INDEX ) {
2016-02-03 13:14:14 -05:00
destination = nextAvailableIndexedName ( destination ) ;
2012-03-09 00:38:22 -05:00
}
}
2013-09-11 11:52:35 -04:00
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 ( ) ) {
2017-04-05 04:42:37 -04:00
log . info ( format ( " [%s] from [%s] to [%s] " , renameAction , source , destination ) ) ;
2012-03-09 00:38:22 -05:00
destination = renameAction . rename ( source , destination ) ;
2016-02-03 13:14:14 -05:00
// remember successfully renamed matches for history entry and possible revert
renameLog . put ( source , destination ) ;
2012-03-09 00:38:22 -05:00
} else {
2016-03-02 10:02:44 -05:00
log . info ( format ( " Skipped [%s] because [%s] already exists " , source , destination ) ) ;
2012-03-09 00:38:22 -05:00
}
2011-11-24 12:27:39 -05:00
} catch ( IOException e ) {
2017-02-17 09:09:51 -05:00
log . warning ( format ( " [%s] Failure: %s " , renameAction , e ) ) ;
2011-11-24 12:27:39 -05:00
throw e ;
}
}
} finally {
2017-04-15 12:43:25 -04:00
// update history and xattr metadata
2017-04-18 03:25:34 -04:00
if ( renameLog . size ( ) > 0 ) {
writeHistory ( renameAction , renameLog , matches ) ;
}
2013-09-11 11:52:35 -04:00
2017-04-15 12:43:25 -04:00
// print number of processed files
2016-03-02 10:02:44 -05:00
log . fine ( format ( " Processed %d files " , renameLog . size ( ) ) ) ;
2011-11-24 12:27:39 -05:00
}
2013-09-11 11:52:35 -04:00
2017-04-18 03:25:34 -04:00
// execute command
if ( exec ! = null ) {
2019-05-18 16:48:10 -04:00
exec . execute ( renameLog . values ( ) . stream ( ) . map ( f - > new MediaBindingBean ( xattr . getMetaInfo ( f ) , f ) ) ) . forEach ( r - > {
// consume stream and ignore exit codes
2019-05-18 16:30:53 -04:00
} ) ;
2017-04-18 03:25:34 -04:00
}
2017-04-15 12:43:25 -04:00
return new ArrayList < File > ( renameLog . values ( ) ) ;
}
protected void writeHistory ( RenameAction action , Map < File , File > log , List < Match < File , ? > > matches ) {
// write rename history
2017-04-18 03:25:34 -04:00
if ( action . canRevert ( ) ) {
HistorySpooler . getInstance ( ) . append ( log . entrySet ( ) ) ;
}
2017-04-15 12:43:25 -04:00
// write xattr metadata
if ( matches ! = null ) {
2016-03-27 09:52:59 -04:00
for ( Match < File , ? > match : matches ) {
2017-04-18 03:25:34 -04:00
if ( match . getCandidate ( ) ! = null ) {
2019-01-08 10:57:56 -05:00
File source = match . getValue ( ) ;
File destination = log . get ( source ) ;
2016-03-27 09:52:59 -04:00
if ( destination ! = null & & destination . isFile ( ) ) {
2019-01-08 10:57:56 -05:00
// remember Last Modified date
long timestamp = source . isFile ( ) ? source . lastModified ( ) : destination . lastModified ( ) ;
// store xattr
2017-04-18 03:25:34 -04:00
xattr . setMetaInfo ( destination , match . getCandidate ( ) , match . getValue ( ) . getName ( ) ) ;
2019-01-08 10:57:56 -05:00
// restore Last Modified date
destination . setLastModified ( timestamp ) ;
2014-01-08 12:23:04 -05:00
}
}
}
}
2016-02-03 13:14:14 -05:00
}
2017-04-15 12:43:25 -04:00
protected File nextAvailableIndexedName ( File file ) {
2016-02-03 13:14:14 -05:00
File parent = file . getParentFile ( ) ;
String name = getName ( file ) ;
String ext = getExtension ( file ) ;
2016-02-09 12:16:14 -05:00
return IntStream . range ( 1 , 100 ) . mapToObj ( i - > new File ( parent , name + '.' + i + '.' + ext ) ) . filter ( f - > ! f . exists ( ) ) . findFirst ( ) . get ( ) ;
2011-11-24 12:27:39 -05:00
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
@Override
2016-11-27 17:10:42 -05:00
public List < File > getSubtitles ( Collection < File > files , String query , Language language , SubtitleFormat output , Charset encoding , SubtitleNaming format , boolean strict ) throws Exception {
2014-01-24 11:01:37 -05:00
// ignore anything that is not a video
files = filter ( files , VIDEO_FILES ) ;
2014-04-10 01:55:01 -04:00
// ignore sample files
files = sortByUniquePath ( filter ( files , not ( getClutterFileFilter ( ) ) ) ) ;
2014-01-24 11:01:37 -05:00
2012-03-30 21:42:35 -04:00
// try to find subtitles for each video file
2014-01-24 11:01:37 -05:00
List < File > remainingVideos = new ArrayList < File > ( files ) ;
2013-09-11 11:52:35 -04:00
2012-03-30 21:42:35 -04:00
// parallel download
List < File > subtitleFiles = new ArrayList < File > ( ) ;
2013-09-11 11:52:35 -04:00
2016-03-02 10:02:44 -05:00
log . finest ( format ( " Get [%s] subtitles for %d files " , language . getName ( ) , remainingVideos . size ( ) ) ) ;
2012-03-30 21:42:35 -04:00
if ( remainingVideos . isEmpty ( ) ) {
2014-07-17 03:08:23 -04:00
throw new CmdlineException ( " No video files: " + files ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// lookup subtitles by hash
2015-08-27 13:55:24 -04:00
for ( VideoHashSubtitleService service : getVideoHashSubtitleServices ( language . getLocale ( ) ) ) {
2016-11-27 17:10:42 -05:00
if ( remainingVideos . isEmpty ( ) | | ! requireLogin ( service ) ) {
2012-03-30 21:42:35 -04:00
continue ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2011-11-24 12:27:39 -05:00
try {
2016-03-02 10:02:44 -05:00
log . fine ( " Looking up subtitles by hash via " + service . getName ( ) ) ;
2017-01-13 15:32:42 -05:00
Map < File , List < SubtitleDescriptor > > options = lookupSubtitlesByHash ( service , remainingVideos , language . getLocale ( ) , false , strict ) ;
2016-11-27 17:10:42 -05:00
Map < File , File > downloads = downloadSubtitleBatch ( service , options , output , encoding , format ) ;
2012-03-30 21:42:35 -04:00
remainingVideos . removeAll ( downloads . keySet ( ) ) ;
subtitleFiles . addAll ( downloads . values ( ) ) ;
2011-11-27 09:39:58 -05:00
} catch ( Exception e ) {
2016-03-02 10:02:44 -05:00
log . warning ( " Lookup by hash failed: " + e . getMessage ( ) ) ;
2011-10-29 16:24:01 -04:00
}
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
for ( SubtitleProvider service : getSubtitleProviders ( language . getLocale ( ) ) ) {
if ( strict | | remainingVideos . isEmpty ( ) | | ! requireLogin ( service ) ) {
2014-01-24 11:01:37 -05:00
continue ;
2012-03-30 21:42:35 -04:00
}
2013-09-11 11:52:35 -04:00
2014-01-24 11:01:37 -05:00
try {
2016-03-02 10:02:44 -05:00
log . fine ( format ( " Looking up subtitles by name via %s " , service . getName ( ) ) ) ;
2017-01-13 15:32:42 -05:00
Map < File , List < SubtitleDescriptor > > options = findSubtitlesByName ( service , remainingVideos , language . getLocale ( ) , query , false , strict ) ;
2016-11-27 17:10:42 -05:00
Map < File , File > downloads = downloadSubtitleBatch ( service , options , output , encoding , format ) ;
2014-01-24 11:01:37 -05:00
remainingVideos . removeAll ( downloads . keySet ( ) ) ;
subtitleFiles . addAll ( downloads . values ( ) ) ;
} catch ( Exception e ) {
2016-03-02 10:02:44 -05:00
log . warning ( format ( " Search by name failed: %s " , e . getMessage ( ) ) ) ;
2011-10-29 16:24:01 -04:00
}
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// no subtitles for remaining video files
2012-03-30 21:42:35 -04:00
for ( File it : remainingVideos ) {
2016-03-02 10:02:44 -05:00
log . warning ( " No matching subtitles found: " + it ) ;
2011-11-24 12:27:39 -05:00
}
2016-02-27 00:22:46 -05:00
2011-11-24 12:27:39 -05:00
return subtitleFiles ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
protected static boolean requireLogin ( Object service ) {
2014-10-28 13:22:48 -04:00
if ( service instanceof OpenSubtitlesClient ) {
OpenSubtitlesClient osdb = ( OpenSubtitlesClient ) service ;
if ( osdb . isAnonymous ( ) ) {
2014-10-29 00:21:33 -04:00
throw new CmdlineException ( String . format ( " %s: Please enter your login details by calling `filebot -script fn:configure` " , osdb . getName ( ) ) ) ;
2014-10-28 13:22:48 -04:00
}
}
return true ; // no login => logged in by default
}
2012-07-21 11:49:22 -04:00
@Override
2016-11-27 17:10:42 -05:00
public List < File > getMissingSubtitles ( Collection < File > files , String query , Language language , SubtitleFormat output , Charset encoding , SubtitleNaming format , boolean strict ) throws Exception {
2011-11-28 07:47:11 -05:00
List < File > videoFiles = filter ( filter ( files , VIDEO_FILES ) , new FileFilter ( ) {
2013-09-11 11:52:35 -04:00
2011-11-28 07:47:11 -05:00
// save time on repeating filesystem calls
2016-05-03 07:38:07 -04:00
private Map < File , List < File > > cache = new HashMap < File , List < File > > ( ) ;
2013-09-11 11:52:35 -04:00
2014-04-04 03:35:30 -04:00
public boolean matchesLanguageCode ( File f ) {
2016-11-27 17:10:42 -05:00
Language languageSuffix = Language . getLanguage ( releaseInfo . getSubtitleLanguageTag ( getName ( f ) ) ) ;
if ( languageSuffix ! = null ) {
return languageSuffix . getCode ( ) . equals ( language . getCode ( ) ) ;
2014-04-04 03:35:30 -04:00
}
return false ;
}
2013-09-11 11:52:35 -04:00
2011-11-28 07:47:11 -05:00
@Override
public boolean accept ( File video ) {
2016-11-27 17:10:42 -05:00
if ( ! video . isFile ( ) ) {
return false ;
}
2016-05-03 07:38:07 -04:00
List < File > subtitleFiles = cache . computeIfAbsent ( video . getParentFile ( ) , parent - > {
return getChildren ( parent , SUBTITLE_FILES ) ;
} ) ;
// can't tell which subtitle belongs to which file -> if any subtitles exist skip the whole folder
2016-11-27 17:10:42 -05:00
if ( format = = SubtitleNaming . ORIGINAL ) {
2016-05-03 07:38:07 -04:00
return subtitleFiles . size ( ) = = 0 ;
2011-11-28 07:47:11 -05:00
}
2013-09-11 11:52:35 -04:00
2016-05-03 07:38:07 -04:00
return subtitleFiles . stream ( ) . allMatch ( f - > {
if ( isDerived ( f , video ) ) {
2016-11-27 17:10:42 -05:00
return format ! = SubtitleNaming . MATCH_VIDEO & & ! matchesLanguageCode ( f ) ;
2014-01-02 14:22:05 -05:00
}
2016-05-03 07:38:07 -04:00
return true ;
} ) ;
2011-11-28 07:47:11 -05:00
}
} ) ;
2013-09-11 11:52:35 -04:00
2011-11-28 07:47:11 -05:00
if ( videoFiles . isEmpty ( ) ) {
2016-03-02 10:02:44 -05:00
log . info ( " No missing subtitles " ) ;
2011-11-28 07:47:11 -05:00
return emptyList ( ) ;
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
return getSubtitles ( videoFiles , query , language , output , encoding , format , strict ) ;
2012-03-30 21:42:35 -04:00
}
2013-09-11 11:52:35 -04:00
2016-04-30 10:59:51 -04:00
private Map < File , File > downloadSubtitleBatch ( Datasource service , Map < File , List < SubtitleDescriptor > > subtitles , SubtitleFormat outputFormat , Charset outputEncoding , SubtitleNaming naming ) {
2019-02-18 13:13:53 -05:00
// flush all memory caches to disk (before starting any long running file system operations that might be cancelled by the user)
CacheManager . getInstance ( ) . flushAll ( ) ;
2016-02-27 00:22:46 -05:00
Map < File , File > downloads = new LinkedHashMap < File , File > ( ) ;
2013-09-11 11:52:35 -04:00
2012-03-30 21:42:35 -04:00
// fetch subtitle
2016-02-27 00:22:46 -05:00
subtitles . forEach ( ( movie , options ) - > {
if ( options . size ( ) > 0 ) {
SubtitleDescriptor subtitle = options . get ( 0 ) ;
try {
2016-04-30 10:59:51 -04:00
downloads . put ( movie , downloadSubtitle ( service , subtitle , movie , outputFormat , outputEncoding , naming ) ) ;
2016-02-27 00:22:46 -05:00
} catch ( Exception e ) {
2017-02-04 10:21:55 -05:00
log . warning ( format ( " Failed to download %s: %s " , subtitle , e ) ) ;
2016-02-27 00:22:46 -05:00
}
2012-03-30 21:42:35 -04:00
}
2016-02-27 00:22:46 -05:00
} ) ;
2013-09-11 11:52:35 -04:00
2012-03-30 21:42:35 -04:00
return downloads ;
2011-11-28 07:47:11 -05:00
}
2013-09-11 11:52:35 -04:00
2016-04-30 10:59:51 -04:00
private File downloadSubtitle ( Datasource service , SubtitleDescriptor descriptor , File movieFile , SubtitleFormat outputFormat , Charset outputEncoding , SubtitleNaming naming ) throws Exception {
2011-10-29 16:24:01 -04:00
// fetch subtitle archive
2016-04-30 10:59:51 -04:00
log . config ( format ( " Fetching [%s] subtitles [%s] from [%s] " , descriptor . getLanguageName ( ) , descriptor . getPath ( ) , service . getName ( ) ) ) ;
2011-11-25 13:52:31 -05:00
MemoryFile subtitleFile = fetchSubtitle ( descriptor ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// subtitle filename is based on movie filename
2016-11-27 17:10:42 -05:00
String extension = getExtension ( subtitleFile . getName ( ) ) ;
2011-10-29 16:24:01 -04:00
ByteBuffer data = subtitleFile . getData ( ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
if ( outputFormat ! = null | | outputEncoding ! = null ) {
2016-11-27 17:10:42 -05:00
// adjust extension of the output file
2011-10-29 16:24:01 -04:00
if ( outputFormat ! = null ) {
2016-11-27 17:10:42 -05:00
extension = outputFormat . getFilter ( ) . extension ( ) ;
}
// default to UTF-8 if no other encoding is given
if ( outputEncoding = = null ) {
outputEncoding = UTF_8 ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
log . finest ( format ( " Export [%s] as [%s / %s] " , subtitleFile . getName ( ) , outputFormat , outputEncoding ) ) ;
2011-10-29 16:24:01 -04:00
data = exportSubtitles ( subtitleFile , outputFormat , 0 , outputEncoding ) ;
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
File destination = new File ( movieFile . getParentFile ( ) , naming . format ( movieFile , descriptor , extension ) ) ;
2016-03-02 10:02:44 -05:00
log . info ( format ( " Writing [%s] to [%s] " , subtitleFile . getName ( ) , destination . getName ( ) ) ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
writeFile ( data , destination ) ;
return destination ;
}
2013-09-11 11:52:35 -04:00
2017-03-23 04:53:20 -04:00
protected < T > List < T > applyExpressionFilter ( List < T > input , ExpressionFilter filter ) {
2013-09-11 11:52:35 -04:00
if ( filter = = null ) {
2017-02-12 06:56:39 -05:00
return input ;
2013-09-11 11:52:35 -04:00
}
2019-06-10 11:47:38 -04:00
log . fine ( formatSingleLine ( " Apply filter [%s] on [%d] items " , filter . getExpression ( ) , input . size ( ) ) ) ;
2017-03-23 04:53:20 -04:00
2019-06-10 03:07:09 -04:00
// support context bindings
Map < File , ? > context = new EntryList < File , T > ( null , input ) ;
2017-03-23 04:53:20 -04:00
return input . stream ( ) . filter ( it - > {
2019-06-10 03:07:09 -04:00
if ( filter . matches ( new MediaBindingBean ( it , null , context ) ) ) {
2016-03-02 10:02:44 -05:00
log . finest ( format ( " Include [%s] " , it ) ) ;
2017-03-23 04:53:20 -04:00
return true ;
2013-09-11 11:52:35 -04:00
}
2017-03-23 04:53:20 -04:00
return false ;
} ) . collect ( toList ( ) ) ;
2013-09-11 11:52:35 -04:00
}
2019-06-10 03:07:09 -04:00
protected List < Episode > applyEpisodeExpressionMapper ( List < Episode > episodes , ExpressionMapper mapper ) {
2019-05-27 04:25:22 -04:00
if ( mapper = = null ) {
2019-06-10 03:07:09 -04:00
return episodes ;
2019-05-27 04:25:22 -04:00
}
2019-06-10 11:47:38 -04:00
log . fine ( formatSingleLine ( " Apply mapper [%s] on [%d] items " , mapper . getExpression ( ) , episodes . size ( ) ) ) ;
2019-06-10 03:07:09 -04:00
// support episode list context
Map < File , Episode > context = new EntryList < File , Episode > ( null , episodes ) ;
2019-05-27 04:25:22 -04:00
2019-06-10 03:07:09 -04:00
return episodes . stream ( ) . map ( episode - > {
2019-05-27 04:25:22 -04:00
try {
2019-06-10 03:07:09 -04:00
Episode mapping = mapper . map ( new MediaBindingBean ( episode , null , context ) , Episode . class ) ;
log . finest ( format ( " Map [%s] to [%s] " , episode , mapping ) ) ;
return new MappedEpisode ( episode , mapping ) ;
2019-05-27 04:25:22 -04:00
} catch ( Exception e ) {
2019-06-10 03:07:09 -04:00
debug . warning ( format ( " Exclude [%s] due to map failure: %s " , episode , e ) ) ;
2019-05-27 04:25:22 -04:00
return null ;
}
} ) . filter ( Objects : : nonNull ) . distinct ( ) . collect ( toList ( ) ) ;
}
2017-02-05 11:26:24 -05:00
protected < T extends SearchResult > T selectSearchResult ( String query , Collection < T > options ) throws Exception {
List < T > matches = selectSearchResult ( query , options , false , false , false , 1 ) ;
2017-02-04 13:07:35 -05:00
return matches . size ( ) > 0 ? matches . get ( 0 ) : null ;
}
2017-02-05 11:26:24 -05:00
protected < T extends SearchResult > List < T > selectSearchResult ( String query , Collection < T > options , boolean sort , boolean alias , boolean strict , int limit ) throws Exception {
List < T > probableMatches = getProbableMatches ( sort ? query : null , options , alias , strict ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
if ( probableMatches . isEmpty ( ) | | ( strict & & probableMatches . size ( ) ! = 1 ) ) {
2012-07-09 15:13:16 -04:00
// allow single search results to just pass through in non-strict mode even if match confidence is low
2016-02-08 17:29:50 -05:00
if ( options . size ( ) = = 1 & & ! strict ) {
2017-02-05 11:26:24 -05:00
return options . stream ( ) . collect ( toList ( ) ) ;
2012-07-09 15:13:16 -04:00
}
2013-09-11 11:52:35 -04:00
2013-04-16 03:32:04 -04:00
if ( strict ) {
2017-02-04 13:07:35 -05:00
throw new CmdlineException ( " Multiple options: Advanced auto-selection requires -non-strict matching: " + probableMatches ) ;
2013-04-16 03:32:04 -04:00
}
2014-03-27 03:40:30 -04:00
2017-02-04 13:07:35 -05:00
// just pick the best N matches
2017-02-05 11:26:24 -05:00
if ( sort ) {
probableMatches = sortBySimilarity ( options , singleton ( query ) , getSeriesMatchMetric ( ) ) . stream ( ) . collect ( toList ( ) ) ;
2014-05-06 14:49:41 -04:00
}
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// return first and only value
2017-02-04 13:07:35 -05:00
return probableMatches . size ( ) < = limit ? probableMatches : probableMatches . subList ( 0 , limit ) ; // trust that the correct match is in the Top N
2011-11-24 12:27:39 -05:00
}
2013-09-11 11:52:35 -04: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 ;
2013-09-11 11:52:35 -04:00
2017-01-12 09:18:10 -05:00
for ( File it : filter ( files , VERIFICATION_FILES ) ) {
2011-10-29 16:24:01 -04:00
result & = check ( it , it . getParentFile ( ) ) ;
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
return result ;
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
@Override
2019-05-27 03:36:25 -04:00
public File compute ( Collection < File > files , HashType hash , File output , Charset encoding ) throws Exception {
2014-01-25 02:36:01 -05:00
// ignore folders and any sort of special files
files = filter ( files , FILES ) ;
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
if ( files . isEmpty ( ) ) {
throw new CmdlineException ( " No files: " + files ) ;
}
2014-01-25 02:36:01 -05:00
// find common parent folder of all files
File [ ] fileList = files . toArray ( new File [ 0 ] ) ;
File [ ] [ ] pathArray = new File [ fileList . length ] [ ] ;
for ( int i = 0 ; i < fileList . length ; i + + ) {
pathArray [ i ] = listPath ( fileList [ i ] . getParentFile ( ) ) . toArray ( new File [ 0 ] ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2014-01-25 02:36:01 -05:00
CommonSequenceMatcher csm = new CommonSequenceMatcher ( null , 0 , true ) ;
File [ ] common = csm . matchFirstCommonSequence ( pathArray ) ;
if ( common = = null ) {
2016-11-27 17:10:42 -05:00
throw new CmdlineException ( " All paths must be on the same filesystem: " + files ) ;
2014-01-25 02:36:01 -05:00
}
// last element in the common sequence must be the root folder
File root = common [ common . length - 1 ] ;
2016-11-27 17:10:42 -05:00
if ( output = = null ) {
output = new File ( root , root . getName ( ) + '.' + hash . getFilter ( ) . extension ( ) ) ;
} else if ( ! output . isAbsolute ( ) ) {
output = new File ( root , output . getPath ( ) ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
log . info ( format ( " Compute %s hash for %s files [%s] " , hash , files . size ( ) , output ) ) ;
compute ( root , files , output , hash , encoding ) ;
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
return output ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
private boolean check ( File verificationFile , File root ) throws Exception {
HashType type = getHashType ( verificationFile ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// check if type is supported
2011-11-28 04:16:27 -05:00
if ( type = = null ) {
2014-07-17 03:08:23 -04:00
throw new CmdlineException ( " Unsupported format: " + verificationFile ) ;
2011-11-28 04:16:27 -05:00
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
// add all file names from verification file
2016-03-02 10:02:44 -05:00
log . fine ( format ( " Checking [%s] " , verificationFile . getName ( ) ) ) ;
2011-10-29 16:24:01 -04:00
VerificationFileReader parser = new VerificationFileReader ( createTextReader ( verificationFile ) , type . getFormat ( ) ) ;
boolean status = true ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
try {
while ( parser . hasNext ( ) ) {
try {
Entry < File , String > it = parser . next ( ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
File file = new File ( root , it . getKey ( ) . getPath ( ) ) . getAbsoluteFile ( ) ;
String current = computeHash ( new File ( root , it . getKey ( ) . getPath ( ) ) , type ) ;
2016-03-02 10:02:44 -05:00
log . info ( format ( " %s %s " , current , file ) ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
if ( current . compareToIgnoreCase ( it . getValue ( ) ) ! = 0 ) {
2016-03-02 10:03:11 -05:00
throw new IOException ( String . format ( " Corrupted file found: %s [hash mismatch: %s vs %s] " , it . getKey ( ) , current , it . getValue ( ) ) ) ;
2011-10-29 16:24:01 -04:00
}
} catch ( IOException e ) {
status = false ;
2016-03-02 10:02:44 -05:00
log . warning ( e . getMessage ( ) ) ;
2011-10-29 16:24:01 -04:00
}
}
} finally {
parser . close ( ) ;
}
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
return status ;
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
private void compute ( File root , Collection < File > files , File outputFile , HashType hashType , Charset encoding ) throws IOException , Exception {
2011-10-29 16:24:01 -04:00
// compute hashes recursively and write to file
2016-11-27 17:10:42 -05:00
VerificationFileWriter out = new VerificationFileWriter ( outputFile , hashType . getFormat ( ) , encoding ! = null ? encoding : UTF_8 ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
try {
for ( File it : files ) {
2017-01-12 09:18:10 -05:00
if ( it . isHidden ( ) | | VERIFICATION_FILES . accept ( it ) ) {
2011-10-29 16:24:01 -04:00
continue ;
2017-01-12 09:18:10 -05:00
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
String relativePath = normalizePathSeparators ( it . getPath ( ) . substring ( root . getPath ( ) . length ( ) + 1 ) ) ; // skip root and first slash
2011-10-29 16:24:01 -04:00
String hash = computeHash ( it , hashType ) ;
2016-03-02 10:02:44 -05:00
log . info ( format ( " %s %s " , hash , relativePath ) ) ;
2013-09-11 11:52:35 -04:00
2011-10-29 16:24:01 -04:00
out . write ( relativePath , hash ) ;
}
} catch ( Exception e ) {
outputFile . deleteOnExit ( ) ; // delete only partially written files
throw e ;
} finally {
out . close ( ) ;
}
}
2013-09-11 11:52:35 -04:00
2019-05-27 04:25:22 -04:00
private List < Episode > fetchEpisodeList ( EpisodeListProvider db , String query , ExpressionFilter filter , ExpressionMapper mapper , SortOrder order , Locale locale , boolean strict ) throws Exception {
2017-02-23 07:10:12 -05:00
// sanity check
if ( query = = null ) {
throw new CmdlineException ( String . format ( " %s: query parameter is required " , db . getName ( ) ) ) ;
}
2013-09-11 11:52:35 -04:00
2017-02-23 11:13:43 -05:00
// collect all episode objects first
List < Episode > episodes = new ArrayList < Episode > ( ) ;
2017-02-23 07:10:12 -05:00
2018-07-08 16:15:26 -04:00
if ( isSeriesID ( query ) ) {
2017-02-23 11:13:43 -05:00
// lookup by id
episodes . addAll ( db . getEpisodeList ( Integer . parseInt ( query ) , order , locale ) ) ;
} else {
// search by name and select search result
List < SearchResult > options = selectSearchResult ( query , db . search ( query , locale ) , false , false , false , strict ? 1 : 5 ) ;
2016-08-11 07:27:05 -04:00
2017-02-23 11:13:43 -05:00
// fetch episodes
for ( SearchResult option : options ) {
episodes . addAll ( db . getEpisodeList ( option , order , locale ) ) ;
}
}
2017-02-12 06:56:39 -05:00
2017-02-23 11:13:43 -05:00
// sanity check
if ( episodes . isEmpty ( ) ) {
throw new CmdlineException ( String . format ( " %s: no results " , db . getName ( ) ) ) ;
2017-02-12 06:56:39 -05:00
}
2019-05-27 04:25:22 -04:00
// filter episodes and apply custom mappings
episodes = applyExpressionFilter ( episodes , filter ) ;
2019-06-10 03:07:09 -04:00
episodes = applyEpisodeExpressionMapper ( episodes , mapper ) ;
2019-05-27 04:25:22 -04:00
return episodes ;
2017-03-22 21:46:41 -04:00
}
2018-07-08 16:15:26 -04:00
private boolean isSeriesID ( String query ) {
return query . matches ( " \\ d{5,9} " ) ;
}
2017-03-22 21:46:41 -04:00
@Override
2019-05-27 04:25:22 -04:00
public Stream < String > fetchEpisodeList ( EpisodeListProvider db , String query , SortOrder order , Locale locale , ExpressionFilter filter , ExpressionMapper mapper , ExpressionFormat format , boolean strict ) throws Exception {
2017-03-22 21:46:41 -04:00
// collect all episode objects first
2019-05-27 04:25:22 -04:00
List < Episode > episodes = fetchEpisodeList ( db , query , filter , mapper , order , locale , strict ) ;
2017-02-12 06:56:39 -05:00
2017-03-23 04:53:20 -04:00
// instant format
if ( format = = null ) {
return episodes . stream ( ) . map ( Episode : : toString ) ;
}
2013-09-11 11:52:35 -04:00
2017-03-23 04:53:20 -04:00
// lazy format
2017-02-23 07:10:12 -05:00
return episodes . stream ( ) . map ( episode - > {
try {
2017-03-23 04:53:20 -04:00
return format . format ( new MediaBindingBean ( episode , null , new EntryList < File , Episode > ( null , episodes ) ) ) ;
2017-02-23 07:10:12 -05:00
} catch ( Exception e ) {
debug . warning ( e : : getMessage ) ;
}
return null ;
} ) . filter ( Objects : : nonNull ) ;
2011-10-29 16:24:01 -04:00
}
2013-09-11 11:52:35 -04:00
2011-11-02 14:19:09 -04:00
@Override
2017-02-23 07:10:12 -05:00
public Stream < String > getMediaInfo ( Collection < File > files , FileFilter filter , ExpressionFormat format ) throws Exception {
// use default expression format if not set
if ( format = = null ) {
2019-03-11 05:53:00 -04:00
return getMediaInfo ( files , filter , new ExpressionFormat ( " {fn} [{resolution} {vc} {channels} {ac} {hours} {mbps}] " ) ) ;
2017-02-23 07:10:12 -05:00
}
2016-11-27 17:10:42 -05:00
2017-02-23 07:10:12 -05:00
return files . stream ( ) . filter ( filter : : accept ) . map ( f - > {
try {
return format . format ( new MediaBindingBean ( xattr . getMetaInfo ( f ) , f ) ) ;
} catch ( Exception e ) {
debug . warning ( e : : getMessage ) ;
}
return null ;
} ) . filter ( Objects : : nonNull ) ;
2011-11-02 14:19:09 -04:00
}
2013-09-11 11:52:35 -04:00
2017-04-18 05:15:59 -04:00
@Override
2019-05-18 16:30:53 -04:00
public IntStream execute ( Collection < File > files , FileFilter filter , ExpressionFormat format , ExecCommand exec ) {
// filter / map / execute
2019-05-18 17:01:12 -04:00
return exec . execute ( files . stream ( ) . filter ( filter : : accept ) . map ( f - > new MediaBindingBean ( xattr . getMetaInfo ( f ) , f ) ) . peek ( b - > {
2019-05-18 16:30:53 -04:00
if ( format ! = null ) {
try {
log . info ( format . format ( b ) ) ;
} catch ( Exception e ) {
debug . warning ( e : : getMessage ) ;
}
}
} ) ) ;
2017-04-18 05:15:59 -04:00
}
2016-03-10 13:32:11 -05:00
@Override
2016-11-27 17:10:42 -05:00
public List < File > revert ( Collection < File > files , FileFilter filter , RenameAction action ) throws Exception {
2016-03-10 14:23:12 -05:00
if ( files . isEmpty ( ) ) {
throw new CmdlineException ( " Expecting at least one input path " ) ;
}
2016-03-10 13:32:11 -05:00
Set < File > whitelist = new HashSet < File > ( files ) ;
Map < File , File > history = HistorySpooler . getInstance ( ) . getCompleteHistory ( ) . getRenameMap ( ) ;
return history . entrySet ( ) . stream ( ) . filter ( it - > {
2016-03-10 23:11:40 -05:00
File original = it . getKey ( ) ;
2016-03-10 13:32:11 -05:00
File current = it . getValue ( ) ;
2017-02-23 07:49:51 -05:00
return Stream . of ( current , original ) . flatMap ( f - > listPath ( f ) . stream ( ) ) . anyMatch ( whitelist : : contains ) & & current . exists ( ) & & filter . accept ( current ) ;
2016-03-10 13:32:11 -05:00
} ) . map ( it - > {
File original = it . getKey ( ) ;
File current = it . getValue ( ) ;
log . info ( format ( " Revert [%s] to [%s] " , current , original ) ) ;
2016-11-27 17:10:42 -05:00
if ( action . canRevert ( ) ) {
try {
return StandardRenameAction . revert ( current , original ) ;
} catch ( Exception e ) {
log . warning ( " Failed to revert file: " + e ) ;
}
2016-03-10 13:32:11 -05:00
}
2016-11-27 17:10:42 -05:00
return null ;
2016-03-10 13:32:11 -05:00
} ) . filter ( Objects : : nonNull ) . collect ( toList ( ) ) ;
}
2012-02-26 07:58:16 -05:00
@Override
2016-11-27 17:10:42 -05:00
public List < File > extract ( Collection < File > files , File output , ConflictAction conflict , FileFilter filter , boolean forceExtractAll ) throws Exception {
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 > ( ) ;
2013-09-11 11:52:35 -04:00
2012-02-26 07:58:16 -05:00
for ( File file : archiveFiles ) {
2015-03-25 18:38:15 -04:00
Archive archive = Archive . open ( file ) ;
2012-02-26 11:57:00 -05:00
try {
2016-11-27 17:10:42 -05:00
File outputFolder = output ;
if ( outputFolder = = null | | ! outputFolder . isAbsolute ( ) ) {
outputFolder = new File ( file . getParentFile ( ) , outputFolder = = null ? getName ( file ) : outputFolder . getPath ( ) ) . getCanonicalFile ( ) ;
2012-05-01 12:33:55 -04:00
}
2013-09-11 11:52:35 -04:00
2016-03-02 10:02:44 -05:00
log . info ( format ( " Read archive [%s] and extract to [%s] " , file . getName ( ) , outputFolder ) ) ;
2019-03-12 11:16:25 -04:00
createFolders ( outputFolder ) ;
2016-08-08 05:05:23 -04:00
FileMapper outputMapper = new FileMapper ( outputFolder ) ;
2013-09-11 11:52:35 -04:00
2016-08-08 05:05:23 -04:00
List < FileInfo > outputMapping = new ArrayList < FileInfo > ( ) ;
2014-01-22 06:31:55 -05:00
for ( FileInfo it : archive . listFiles ( ) ) {
File outputPath = outputMapper . getOutputFile ( it . toFile ( ) ) ;
outputMapping . add ( new SimpleFileInfo ( outputPath . getPath ( ) , it . getLength ( ) ) ) ;
2012-07-31 12:17:15 -04:00
}
2013-09-11 11:52:35 -04:00
2018-10-20 06:29:50 -04:00
// print warning message if archive appears empty
if ( outputMapping . isEmpty ( ) ) {
log . warning ( format ( " [%s] contains [%s] files " , file . getName ( ) , outputMapping . size ( ) ) ) ;
}
2016-08-08 05:05:23 -04:00
Set < FileInfo > selection = new TreeSet < FileInfo > ( ) ;
2014-01-22 06:31:55 -05:00
for ( FileInfo future : outputMapping ) {
if ( filter = = null | | filter . accept ( future . toFile ( ) ) ) {
2012-07-31 12:17:15 -04:00
selection . add ( future ) ;
}
}
2013-09-11 11:52:35 -04:00
2012-07-31 12:17:15 -04:00
// check if there is anything to extract at all
if ( selection . isEmpty ( ) ) {
continue ;
}
2013-09-11 11:52:35 -04:00
2012-03-10 05:24:35 -05:00
boolean skip = true ;
2014-01-22 06:31:55 -05:00
for ( FileInfo future : filter = = null | | forceExtractAll ? outputMapping : selection ) {
2016-11-27 17:10:42 -05:00
if ( conflict = = ConflictAction . AUTO ) {
2014-01-22 06:31:55 -05:00
skip & = ( future . toFile ( ) . exists ( ) & & future . getLength ( ) = = future . toFile ( ) . length ( ) ) ;
} else {
skip & = ( future . toFile ( ) . exists ( ) ) ;
}
2012-02-26 11:57:00 -05:00
}
2013-09-11 11:52:35 -04:00
2016-11-27 17:10:42 -05:00
if ( ! skip | | conflict = = ConflictAction . OVERRIDE ) {
2012-07-31 12:17:15 -04:00
if ( filter = = null | | forceExtractAll ) {
2016-03-02 10:02:44 -05:00
log . finest ( " Extracting files " + outputMapping ) ;
2014-01-22 06:31:55 -05:00
2012-07-31 12:17:15 -04:00
// extract all files
2015-03-25 18:38:15 -04:00
archive . extract ( outputMapper . getOutputDir ( ) ) ;
2014-01-22 06:31:55 -05:00
for ( FileInfo it : outputMapping ) {
extractedFiles . add ( it . toFile ( ) ) ;
}
2012-07-31 12:17:15 -04:00
} else {
2016-03-02 10:02:44 -05:00
log . finest ( " Extracting files " + selection ) ;
2014-01-22 06:31:55 -05:00
2012-07-31 12:17:15 -04:00
// extract files selected by the given filter
2017-09-22 02:53:49 -04:00
archive . extract ( outputMapper . getOutputDir ( ) , outputMapper . newPathFilter ( selection ) ) ;
2014-01-22 06:31:55 -05:00
for ( FileInfo it : selection ) {
extractedFiles . add ( it . toFile ( ) ) ;
}
2012-07-31 12:17:15 -04:00
}
2012-03-10 05:24:35 -05:00
} else {
2016-03-02 10:02:44 -05:00
log . finest ( " Skipped extracting files " + selection ) ;
2012-03-10 05:24:35 -05:00
}
2012-02-26 11:57:00 -05:00
} finally {
archive . close ( ) ;
2012-02-26 07:58:16 -05:00
}
}
2013-09-11 11:52:35 -04:00
2012-02-26 07:58:16 -05:00
return extractedFiles ;
}
2016-03-10 13:32:11 -05:00
2011-10-29 16:24:01 -04:00
}