2009-11-21 14:21:46 -05:00
2011-09-22 01:03:46 -04:00
package net.sourceforge.filebot.ui.rename ;
2009-11-21 14:21:46 -05:00
2012-07-17 13:47:07 -04:00
import static java.awt.Cursor.* ;
2012-07-14 15:04:30 -04:00
import static java.util.Collections.* ;
2009-11-21 14:21:46 -05:00
import static net.sourceforge.filebot.MediaTypes.* ;
2012-06-30 04:46:55 -04:00
import static net.sourceforge.filebot.Settings.* ;
2011-12-26 13:10:53 -05:00
import static net.sourceforge.filebot.media.MediaDetection.* ;
2012-07-14 15:04:30 -04:00
import static net.sourceforge.filebot.similarity.CommonSequenceMatcher.* ;
2012-02-22 03:30:50 -05:00
import static net.sourceforge.filebot.similarity.Normalization.* ;
2009-11-22 07:51:23 -05:00
import static net.sourceforge.tuned.FileUtilities.* ;
2009-11-21 20:27:05 -05:00
import static net.sourceforge.tuned.ui.TunedUtilities.* ;
2009-11-21 14:21:46 -05:00
2011-11-28 05:24:46 -05:00
import java.awt.Component ;
2012-07-14 15:04:30 -04:00
import java.awt.Dimension ;
2009-11-21 14:21:46 -05:00
import java.io.File ;
2012-01-02 04:33:50 -05:00
import java.util.AbstractMap.SimpleEntry ;
2009-11-21 14:21:46 -05:00
import java.util.ArrayList ;
2011-11-14 06:43:22 -05:00
import java.util.Collection ;
2009-11-22 07:51:23 -05:00
import java.util.Comparator ;
2010-01-26 14:08:09 -05:00
import java.util.HashMap ;
2012-01-02 01:09:00 -05:00
import java.util.LinkedList ;
2009-11-21 14:21:46 -05:00
import java.util.List ;
2011-08-08 13:37:45 -04:00
import java.util.Locale ;
2010-01-26 14:08:09 -05:00
import java.util.Map ;
2011-12-05 10:38:41 -05:00
import java.util.Map.Entry ;
2012-02-14 09:16:13 -05:00
import java.util.NoSuchElementException ;
2010-01-26 14:08:09 -05:00
import java.util.SortedSet ;
2012-06-15 06:45:35 -04:00
import java.util.TreeMap ;
2010-01-26 14:08:09 -05:00
import java.util.TreeSet ;
2009-11-21 20:27:05 -05:00
import java.util.concurrent.Callable ;
2012-01-02 04:33:50 -05:00
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.Executors ;
import java.util.concurrent.Future ;
2009-11-21 20:27:05 -05:00
import java.util.concurrent.FutureTask ;
import java.util.concurrent.RunnableFuture ;
2012-02-14 09:16:13 -05:00
import java.util.logging.Level ;
import java.util.logging.Logger ;
2009-11-21 20:27:05 -05:00
import javax.swing.Action ;
2012-07-17 13:47:07 -04:00
import javax.swing.JCheckBox ;
import javax.swing.JComponent ;
2009-11-21 20:27:05 -05:00
import javax.swing.SwingUtilities ;
2009-11-21 14:21:46 -05:00
2011-09-21 09:29:21 -04:00
import net.sourceforge.filebot.Analytics ;
2012-02-14 09:16:13 -05:00
import net.sourceforge.filebot.MediaTypes ;
2012-07-17 13:47:07 -04:00
import net.sourceforge.filebot.ResourceManager ;
2009-11-21 14:21:46 -05:00
import net.sourceforge.filebot.similarity.Match ;
2012-01-02 01:09:00 -05:00
import net.sourceforge.filebot.similarity.NameSimilarityMetric ;
import net.sourceforge.filebot.similarity.SimilarityMetric ;
2009-11-21 20:27:05 -05:00
import net.sourceforge.filebot.ui.SelectDialog ;
2011-09-22 08:55:04 -04:00
import net.sourceforge.filebot.web.Movie ;
2009-11-21 14:21:46 -05:00
import net.sourceforge.filebot.web.MovieIdentificationService ;
2011-09-18 15:08:03 -04:00
import net.sourceforge.filebot.web.MoviePart ;
2012-02-13 04:54:57 -05:00
import net.sourceforge.filebot.web.SortOrder ;
2012-04-29 01:59:12 -04:00
import net.sourceforge.tuned.FileUtilities.ParentFilter ;
2009-11-21 14:21:46 -05:00
class MovieHashMatcher implements AutoCompleteMatcher {
private final MovieIdentificationService service ;
2011-12-05 10:38:41 -05:00
2009-11-21 14:21:46 -05:00
public MovieHashMatcher ( MovieIdentificationService service ) {
this . service = service ;
}
2011-12-05 10:38:41 -05:00
2009-11-21 14:21:46 -05:00
@Override
2012-02-13 04:54:57 -05:00
public List < Match < File , ? > > match ( final List < File > files , final SortOrder sortOrder , final Locale locale , final boolean autodetect , final Component parent ) throws Exception {
2012-06-15 06:45:35 -04:00
// ignore sample files
List < File > fileset = filter ( files , NON_CLUTTER_FILES ) ;
2009-11-22 07:51:23 -05:00
// handle movie files
2012-06-15 06:45:35 -04:00
List < File > movieFiles = filter ( fileset , VIDEO_FILES ) ;
List < File > nfoFiles = filter ( fileset , MediaTypes . getDefaultFilter ( " application/nfo " ) ) ;
2011-12-30 10:34:02 -05: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 ) ;
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 ;
}
2012-01-07 09:43:55 -05:00
}
2011-12-30 10:34:02 -05:00
}
2012-02-10 11:43:09 -05:00
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-07-14 15:04:30 -04:00
final Map < File , Movie > movieByFile = new TreeMap < File , Movie > ( ) ;
2012-02-10 11:43:09 -05:00
if ( movieFiles . size ( ) > 0 ) {
try {
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 ) {
// ignore
}
}
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
2012-04-29 01:59:12 -04:00
SortedSet < File > siblingMovieFiles = new TreeSet < File > ( filter ( movieFiles , new ParentFilter ( nfo . getParentFile ( ) ) ) ) ;
2012-07-11 16:22:09 -04:00
String baseName = stripReleaseInfo ( getName ( nfo ) . toLowerCase ( ) ) ;
2012-02-16 04:42:06 -05:00
2012-02-15 07:40:18 -05:00
for ( File movieFile : siblingMovieFiles ) {
2012-07-11 16:22:09 -04:00
if ( stripReleaseInfo ( getName ( movieFile ) ) . toLowerCase ( ) . startsWith ( baseName ) ) {
2012-02-15 07:40:18 -05:00
movieByFile . put ( movieFile , movie ) ;
}
}
2012-02-14 09:16:13 -05:00
} catch ( NoSuchElementException e ) {
Logger . getLogger ( getClass ( ) . getName ( ) ) . log ( Level . WARNING , " Failed to grep IMDbID: " + nfo . getName ( ) ) ;
}
}
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-06-15 06:45:35 -04:00
movieMatchFiles . addAll ( filter ( files , DISK_FOLDERS ) ) ;
2012-02-14 09:16:13 -05:00
movieMatchFiles . addAll ( filter ( orphanedFiles , SUBTITLE_FILES ) ) ; // run movie detection only on orphaned subtitle files
2012-02-10 11:43:09 -05:00
2012-02-15 07:40:18 -05:00
// match remaining movies file by file in parallel
2012-07-14 15:04:30 -04:00
List < Future < Entry < File , Collection < Movie > > > > grabMovieJobs = new ArrayList < Future < Entry < File , Collection < Movie > > > > ( ) ;
2012-02-15 07:40:18 -05:00
2012-07-14 15:04:30 -04:00
// process in parallel
ExecutorService executor = Executors . newFixedThreadPool ( getPreferredThreadPoolSize ( ) ) ;
2012-06-15 06:45:35 -04:00
2010-01-26 14:08:09 -05:00
// map all files by movie
2012-02-10 11:43:09 -05:00
for ( final File file : movieMatchFiles ) {
2012-07-14 15:04:30 -04:00
if ( movieByFile . containsKey ( file ) )
continue ;
grabMovieJobs . add ( executor . submit ( new Callable < Entry < File , Collection < Movie > > > ( ) {
2011-09-21 09:29:21 -04:00
2012-01-02 04:33:50 -05:00
@Override
2012-07-14 15:04:30 -04:00
public SimpleEntry < File , Collection < Movie > > call ( ) throws Exception {
return new SimpleEntry < File , Collection < Movie > > ( file , detectMovie ( file , null , service , locale , false ) ) ;
2011-09-21 09:29:21 -04:00
}
2012-07-14 15:04:30 -04:00
} ) ) ;
2012-01-02 04:33:50 -05:00
}
2012-07-14 15:04:30 -04:00
// remember user decisions and only bother user once
2012-07-17 13:47:07 -04:00
Map < String , Object > memory = new HashMap < String , Object > ( ) ;
memory . put ( " input " , new TreeMap < String , String > ( getLenientCollator ( locale ) ) ) ;
memory . put ( " selection " , new TreeMap < String , String > ( getLenientCollator ( locale ) ) ) ;
2012-02-14 09:16:13 -05:00
2012-01-02 04:33:50 -05:00
try {
2012-07-14 15:04:30 -04:00
for ( Future < Entry < File , Collection < Movie > > > it : grabMovieJobs ) {
// auto-select movie or ask user
File movieFile = it . get ( ) . getKey ( ) ;
2012-07-17 13:47:07 -04:00
Movie movie = grabMovieName ( movieFile , it . get ( ) . getValue ( ) , locale , autodetect , memory , parent ) ;
if ( movie ! = null ) {
movieByFile . put ( movieFile , movie ) ;
}
2010-01-26 14:08:09 -05:00
}
2012-01-02 04:33:50 -05:00
} finally {
2012-07-17 13:47:07 -04:00
executor . shutdownNow ( ) ;
2010-01-26 14:08:09 -05:00
}
2012-07-14 15:04:30 -04:00
// map movies to (possibly multiple) files (in natural order)
Map < Movie , SortedSet < File > > filesByMovie = new HashMap < Movie , SortedSet < File > > ( ) ;
// collect movie part data
for ( Entry < File , Movie > it : movieByFile . entrySet ( ) ) {
SortedSet < File > movieParts = filesByMovie . get ( it . getValue ( ) ) ;
if ( movieParts = = null ) {
movieParts = new TreeSet < File > ( ) ;
filesByMovie . put ( it . getValue ( ) , movieParts ) ;
}
movieParts . add ( it . getKey ( ) ) ;
}
2010-01-26 14:08:09 -05:00
// collect all File/MoviePart matches
List < Match < File , ? > > matches = new ArrayList < Match < File , ? > > ( ) ;
2011-09-22 08:55:04 -04: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-16 02:19:12 -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-16 02:19:12 -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
}
2009-11-22 07:51:23 -05:00
}
}
}
// restore original order
2012-07-14 15:04:30 -04:00
sort ( matches , new Comparator < Match < File , ? > > ( ) {
2009-11-22 07:51:23 -05:00
@Override
public int compare ( Match < File , ? > o1 , Match < File , ? > o2 ) {
return files . indexOf ( o1 . getValue ( ) ) - files . indexOf ( o2 . getValue ( ) ) ;
}
} ) ;
2009-11-21 14:21:46 -05:00
return matches ;
}
2009-11-21 20:27:05 -05:00
2012-07-17 13:47:07 -04:00
protected Movie grabMovieName ( File movieFile , Collection < Movie > options , Locale locale , boolean autodetect , Map < String , Object > memory , Component parent ) throws Exception {
2011-08-21 23:43:22 -04:00
// allow manual user input
2012-07-17 13:47:07 -04:00
if ( ! autodetect | | options . isEmpty ( ) ) {
if ( autodetect & & memory . containsKey ( " repeat " ) ) {
return null ;
}
2011-12-30 10:34:02 -05:00
String suggestion = options . isEmpty ( ) ? stripReleaseInfo ( getName ( movieFile ) ) : options . iterator ( ) . next ( ) . getName ( ) ;
2011-11-26 04:50:31 -05:00
2012-07-17 13:47:07 -04:00
@SuppressWarnings ( " unchecked " )
Map < String , String > inputMemory = ( Map < String , String > ) memory . get ( " input " ) ;
String input = inputMemory . get ( suggestion ) ;
if ( input = = null | | suggestion = = null | | suggestion . isEmpty ( ) ) {
File movieFolder = guessMovieFolder ( movieFile ) ;
input = showInputDialog ( " Enter movie name: " , suggestion , movieFolder = = null ? movieFile . getName ( ) : String . format ( " %s/%s " , movieFolder . getName ( ) , movieFile . getName ( ) ) , parent ) ;
inputMemory . put ( suggestion , input ) ;
2011-11-26 04:50:31 -05:00
}
2011-07-06 22:08:50 -04:00
2011-08-21 23:43:22 -04:00
if ( input ! = null ) {
2012-07-14 15:04:30 -04:00
options = service . searchMovie ( input , locale ) ;
2011-07-06 22:08:50 -04:00
}
}
2012-07-17 13:47:07 -04:00
return options . isEmpty ( ) ? null : selectMovie ( movieFile , options , memory , parent ) ;
2009-11-21 20:27:05 -05:00
}
2011-12-05 10:38:41 -05:00
2012-07-17 13:47:07 -04:00
protected Movie selectMovie ( final File movieFile , final Collection < Movie > options , final Map < String , Object > memory , final Component parent ) throws Exception {
2012-06-15 06:45:35 -04:00
// 1. movie by filename
final String fileQuery = stripReleaseInfo ( getName ( movieFile ) ) ;
// 2. movie by directory
final File movieFolder = guessMovieFolder ( movieFile ) ;
final String folderQuery = ( movieFolder = = null ) ? " " : stripReleaseInfo ( movieFolder . getName ( ) ) ;
2012-02-22 03:30:50 -05:00
// auto-ignore invalid files
2012-06-15 06:45:35 -04:00
if ( fileQuery . length ( ) < 2 & & folderQuery . length ( ) < 2 ) {
2012-02-22 03:30:50 -05:00
return null ;
}
2009-11-21 20:27:05 -05:00
if ( options . size ( ) = = 1 ) {
2011-12-30 10:34:02 -05:00
return options . iterator ( ) . next ( ) ;
2009-11-21 20:27:05 -05:00
}
2012-02-22 03:30:50 -05:00
// auto-select perfect match
for ( Movie movie : options ) {
String movieIdentifier = normalizePunctuation ( movie . toString ( ) ) . toLowerCase ( ) ;
2012-06-15 06:45:35 -04:00
if ( fileQuery . toLowerCase ( ) . startsWith ( movieIdentifier ) | | folderQuery . toLowerCase ( ) . startsWith ( movieIdentifier ) ) {
2012-02-22 03:30:50 -05:00
return movie ;
}
}
2012-01-02 01:09:00 -05:00
// auto-select most probable search result
final List < Movie > probableMatches = new LinkedList < Movie > ( ) ;
final SimilarityMetric metric = new NameSimilarityMetric ( ) ;
// find probable matches using name similarity >= 0.9
for ( Movie result : options ) {
2012-02-22 03:30:50 -05:00
if ( metric . getSimilarity ( fileQuery , result . getName ( ) ) > = 0 . 9 | | metric . getSimilarity ( folderQuery , result . getName ( ) ) > = 0 . 9 ) {
2012-01-02 01:09:00 -05:00
probableMatches . add ( result ) ;
}
}
// auto-select first and only probable search result
if ( probableMatches . size ( ) = = 1 ) {
return probableMatches . get ( 0 ) ;
}
2009-11-21 20:27:05 -05:00
// show selection dialog on EDT
2011-09-22 08:55:04 -04:00
final RunnableFuture < Movie > showSelectDialog = new FutureTask < Movie > ( new Callable < Movie > ( ) {
2009-11-21 20:27:05 -05:00
@Override
2011-09-22 08:55:04 -04:00
public Movie call ( ) throws Exception {
2009-11-21 20:27:05 -05:00
// multiple results have been found, user must select one
2011-11-28 05:24:46 -05:00
SelectDialog < Movie > selectDialog = new SelectDialog < Movie > ( parent , options ) ;
2009-11-21 20:27:05 -05:00
2012-06-15 08:11:28 -04:00
selectDialog . setTitle ( folderQuery . isEmpty ( ) ? fileQuery : String . format ( " %s / %s " , folderQuery , fileQuery ) ) ;
2012-06-15 06:45:35 -04:00
selectDialog . getHeaderLabel ( ) . setText ( String . format ( " Movies matching '%s': " , fileQuery . length ( ) > = 2 | | folderQuery . length ( ) < = 2 ? fileQuery : folderQuery ) ) ;
2009-11-21 20:27:05 -05:00
selectDialog . getCancelAction ( ) . putValue ( Action . NAME , " Ignore " ) ;
2012-07-14 15:04:30 -04:00
selectDialog . setMinimumSize ( new Dimension ( 280 , 300 ) ) ;
2012-01-01 22:48:24 -05:00
selectDialog . pack ( ) ;
2009-11-21 20:27:05 -05:00
2012-07-17 13:47:07 -04:00
// add repeat button
JCheckBox checkBox = new JCheckBox ( ) ;
2012-07-17 16:55:01 -04:00
checkBox . setToolTipText ( " Auto-Repeat " ) ;
2012-07-17 13:47:07 -04:00
checkBox . setCursor ( getPredefinedCursor ( HAND_CURSOR ) ) ;
checkBox . setIcon ( ResourceManager . getIcon ( " button.repeat " ) ) ;
checkBox . setSelectedIcon ( ResourceManager . getIcon ( " button.repeat.selected " ) ) ;
JComponent c = ( JComponent ) selectDialog . getContentPane ( ) ;
c . add ( checkBox , " pos 1al select.y n select.y2 " ) ;
2009-11-21 20:27:05 -05:00
// show dialog
selectDialog . setLocation ( getOffsetLocation ( selectDialog . getOwner ( ) ) ) ;
selectDialog . setVisible ( true ) ;
2012-07-17 13:47:07 -04:00
// remember if we should auto-repeat the chosen action in the future
if ( checkBox . isSelected ( ) ) {
memory . put ( " repeat " , selectDialog . getSelectedValue ( ) ! = null ? " select " : " ignore " ) ;
}
2009-11-21 20:27:05 -05:00
// selected value or null if the dialog was canceled by the user
return selectDialog . getSelectedValue ( ) ;
}
} ) ;
// allow only one select dialog at a time
2012-07-17 13:47:07 -04:00
@SuppressWarnings ( " unchecked " )
Map < String , Movie > selectionMemory = ( Map < String , Movie > ) memory . get ( " selection " ) ;
if ( selectionMemory . containsKey ( fileQuery ) ) {
return selectionMemory . get ( fileQuery ) ;
}
// check auto-selection settings
if ( " select " . equals ( memory . get ( " repeat " ) ) ) {
return options . iterator ( ) . next ( ) ;
}
if ( " ignore " . equals ( memory . get ( " repeat " ) ) ) {
return null ;
2009-11-21 20:27:05 -05:00
}
2012-07-17 13:47:07 -04:00
// ask for user input
SwingUtilities . invokeAndWait ( showSelectDialog ) ;
// cache selected value
selectionMemory . put ( fileQuery , showSelectDialog . get ( ) ) ;
return showSelectDialog . get ( ) ;
2009-11-21 20:27:05 -05:00
}
2009-11-21 14:21:46 -05:00
}