2014-04-19 02:30:29 -04:00
package net.filebot.similarity ;
2009-07-26 12:54:24 -04:00
2016-03-13 13:35:34 -04:00
import static java.util.Arrays.* ;
2013-09-11 13:22:00 -04:00
import static java.util.Collections.* ;
2013-10-29 14:34:39 -04:00
import static java.util.regex.Pattern.* ;
2016-03-29 04:00:09 -04:00
import static java.util.stream.Collectors.* ;
2016-08-09 15:11:06 -04:00
import static net.filebot.Logging.* ;
2016-01-08 07:26:42 -05:00
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.similarity.Normalization.* ;
import static net.filebot.util.FileUtilities.* ;
2015-05-25 06:37:42 -04:00
import static net.filebot.util.StringUtilities.* ;
2009-08-10 07:46:24 -04:00
2009-07-26 12:54:24 -04:00
import java.io.File ;
2016-08-09 17:50:54 -04:00
import java.time.Instant ;
2016-02-22 16:29:26 -05:00
import java.time.LocalDate ;
import java.time.temporal.ChronoUnit ;
2009-07-26 12:54:24 -04:00
import java.util.Collection ;
2019-02-05 02:45:57 -05:00
import java.util.HashMap ;
2013-10-29 14:34:39 -04:00
import java.util.HashSet ;
2013-02-15 04:50:23 -05:00
import java.util.List ;
2011-11-13 13:29:25 -05:00
import java.util.Map ;
2019-02-05 03:10:36 -05:00
import java.util.Optional ;
2013-10-29 14:34:39 -04:00
import java.util.Set ;
import java.util.regex.Matcher ;
import java.util.regex.Pattern ;
2016-09-17 15:31:27 -04:00
import java.util.stream.Stream ;
2009-07-26 12:54:24 -04:00
2016-03-13 13:35:34 -04:00
import com.ibm.icu.text.Transliterator ;
2018-12-06 02:56:26 -05:00
import net.filebot.media.MediaCharacteristics ;
import net.filebot.media.MediaCharacteristicsParser ;
2014-04-19 02:30:29 -04:00
import net.filebot.media.SmartSeasonEpisodeMatcher ;
import net.filebot.similarity.SeasonEpisodeMatcher.SxE ;
import net.filebot.vfs.FileInfo ;
import net.filebot.web.Episode ;
import net.filebot.web.EpisodeFormat ;
import net.filebot.web.Movie ;
2014-12-10 13:53:58 -05:00
import net.filebot.web.SeriesInfo ;
2014-06-24 06:59:00 -04:00
import net.filebot.web.SimpleDate ;
2009-07-26 12:54:24 -04:00
2019-02-05 02:45:57 -05:00
public class EpisodeMetrics {
2013-09-06 03:55:13 -04:00
2009-07-26 12:54:24 -04:00
// Match by season / episode numbers
2019-02-05 02:45:57 -05:00
public final SimilarityMetric SeasonEpisode = new SeasonEpisodeMetric ( new SmartSeasonEpisodeMatcher ( null , false ) ) {
2013-09-06 03:55:13 -04:00
2019-02-05 03:10:36 -05:00
private final Map < Object , Collection < SxE > > cache = synchronizedMap ( new HashMap < > ( 64 , 4 ) ) ;
2013-09-06 03:55:13 -04:00
2009-07-26 12:54:24 -04:00
@Override
protected Collection < SxE > parse ( Object object ) {
2017-02-17 06:49:53 -05:00
// SxE sets for Episode objects cannot be cached because the same Episode (by ID) may have different episode numbers depending on the order (e.g. Airdate VS DVD order)
if ( object instanceof Episode ) {
Episode episode = ( Episode ) object ;
return parse ( episode ) ;
}
2013-09-06 03:55:13 -04:00
2017-02-17 06:51:28 -05:00
if ( object instanceof Movie ) {
return emptySet ( ) ;
}
2019-02-05 03:10:36 -05:00
return cache . computeIfAbsent ( object , o - > {
Collection < SxE > sxe = super . parse ( o ) ;
return sxe = = null ? emptySet ( ) : sxe ;
} ) ;
2016-08-17 03:23:09 -04:00
}
2013-09-06 03:55:13 -04:00
2016-08-17 03:23:09 -04:00
private Set < SxE > parse ( Episode e ) {
// get SxE from episode, both SxE for season/episode numbering and SxE for absolute episode numbering
Set < SxE > sxe = new HashSet < SxE > ( 2 ) ;
// default SxE numbering
if ( e . getEpisode ( ) ! = null ) {
sxe . add ( new SxE ( e . getSeason ( ) , e . getEpisode ( ) ) ) ;
2013-09-06 03:55:13 -04:00
2014-03-18 02:08:46 -04:00
// absolute numbering
2016-08-17 03:23:09 -04:00
if ( e . getAbsolute ( ) ! = null ) {
sxe . add ( new SxE ( null , e . getAbsolute ( ) ) ) ;
2014-03-18 02:08:46 -04:00
}
2016-08-17 03:23:09 -04:00
} else {
2014-03-18 02:08:46 -04:00
// 0xSpecial numbering
2016-08-17 03:23:09 -04:00
if ( e . getSpecial ( ) ! = null ) {
sxe . add ( new SxE ( 0 , e . getSpecial ( ) ) ) ;
2014-03-18 02:08:46 -04:00
}
2010-10-24 12:33:38 -04:00
}
2013-09-06 03:55:13 -04:00
2016-08-17 03:23:09 -04:00
return sxe ;
2010-10-24 12:33:38 -04:00
}
2016-08-17 03:23:09 -04:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " SeasonEpisode " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2010-10-24 12:33:38 -04:00
// Match episode airdate
2019-02-05 02:45:57 -05:00
public final SimilarityMetric AirDate = new DateMetric ( getDateMatcher ( ) ) {
2013-09-06 03:55:13 -04:00
2019-02-05 03:10:36 -05:00
private final Map < Object , Optional < SimpleDate > > cache = synchronizedMap ( new HashMap < > ( 64 , 4 ) ) ;
2013-09-06 03:55:13 -04:00
2010-10-24 12:33:38 -04:00
@Override
2014-04-19 01:39:52 -04:00
public SimpleDate parse ( Object object ) {
2010-10-24 12:33:38 -04:00
if ( object instanceof Episode ) {
Episode episode = ( Episode ) object ;
2013-07-13 06:01:33 -04:00
return episode . getAirdate ( ) ;
2009-07-26 12:54:24 -04:00
}
2013-09-06 03:55:13 -04:00
2017-02-18 11:41:36 -05:00
if ( object instanceof Movie ) {
return null ;
2011-11-13 13:29:25 -05:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 03:10:36 -05:00
return cache . computeIfAbsent ( object , o - > {
return Optional . ofNullable ( super . parse ( o ) ) ;
} ) . orElse ( null ) ;
2009-07-26 12:54:24 -04:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " AirDate " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2011-11-22 11:08:36 -05:00
// Match by episode/movie title
2019-02-05 02:45:57 -05:00
public final SimilarityMetric Title = new SubstringMetric ( ) {
2013-09-06 03:55:13 -04:00
2011-11-22 11:08:36 -05:00
@Override
protected String normalize ( Object object ) {
if ( object instanceof Episode ) {
2012-02-08 08:16:41 -05:00
Episode e = ( Episode ) object ;
2013-09-06 03:55:13 -04:00
2012-02-08 08:16:41 -05:00
// don't use title for matching if title equals series name
2016-05-12 11:54:49 -04:00
if ( e . getTitle ( ) ! = null ) {
String title = normalizeObject ( removeTrailingBrackets ( e . getTitle ( ) ) ) ;
if ( title . length ( ) > = 4 & & ! normalizeObject ( e . getSeriesName ( ) ) . contains ( title ) ) {
return title ;
}
2012-02-08 08:16:41 -05:00
}
2011-11-22 11:08:36 -05:00
}
2013-09-06 03:55:13 -04:00
2011-11-22 11:08:36 -05:00
if ( object instanceof Movie ) {
2014-02-27 14:48:31 -05:00
return normalizeObject ( ( ( Movie ) object ) . getName ( ) ) ;
2011-11-22 11:08:36 -05:00
}
2013-09-06 03:55:13 -04:00
2014-02-27 14:48:31 -05:00
String s = normalizeObject ( object ) ;
return s . length ( ) > = 4 ? s : null ; // only consider long enough strings to avoid false matches
2011-11-22 11:08:36 -05:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " Title " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2011-11-22 13:26:50 -05:00
// Match by SxE and airdate
2019-02-05 02:45:57 -05:00
public final SimilarityMetric EpisodeIdentifier = new MetricCascade ( SeasonEpisode , AirDate ) ;
2013-09-06 03:55:13 -04:00
// Advanced episode <-> file matching Lv1
2019-02-05 02:45:57 -05:00
public final SimilarityMetric EpisodeFunnel = new MetricCascade ( SeasonEpisode , AirDate , Title ) ;
2013-09-06 03:55:13 -04:00
2013-02-21 02:42:29 -05:00
// Advanced episode <-> file matching Lv2
2019-02-05 02:45:57 -05:00
public final SimilarityMetric EpisodeBalancer = new SimilarityMetric ( ) {
2013-09-06 03:55:13 -04:00
2011-11-22 13:26:50 -05:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
float sxe = EpisodeIdentifier . getSimilarity ( o1 , o2 ) ;
2014-09-11 14:21:23 -04:00
float title = sxe < 1 ? Title . getSimilarity ( o1 , o2 ) : 1 ; // if SxE matches then boost score as if it was a title match as well
2013-09-06 03:55:13 -04:00
2013-03-27 02:06:10 -04:00
// account for misleading SxE patterns in the episode title
if ( sxe < 0 & & title = = 1 & & EpisodeIdentifier . getSimilarity ( getTitle ( o1 ) , getTitle ( o2 ) ) = = 1 ) {
sxe = 1 ;
title = 0 ;
}
2013-09-06 03:55:13 -04:00
2013-09-27 05:08:20 -04:00
// allow title to override SxE only if series name also is a good match
if ( title = = 1 & & SeriesName . getSimilarity ( o1 , o2 ) < 0 . 5f ) {
title = 0 ;
}
2011-11-22 13:26:50 -05:00
// 1:SxE && Title, 2:SxE
2016-03-20 16:25:59 -04:00
return ( float ) ( ( Math . max ( sxe , 0 ) * title ) + ( Math . floor ( sxe ) / 10 ) ) ;
2011-11-22 13:26:50 -05:00
}
2013-09-06 03:55:13 -04:00
2013-03-27 02:06:10 -04:00
public Object getTitle ( Object o ) {
if ( o instanceof Episode ) {
2013-03-27 08:33:23 -04:00
Episode e = ( Episode ) o ;
2017-02-17 06:49:53 -05:00
return e . getSeriesName ( ) + " " + e . getTitle ( ) ;
2013-03-27 02:06:10 -04:00
}
return o ;
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " EpisodeBalancer " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2011-10-28 02:28:19 -04:00
// Match series title and episode title against folder structure and file name
2019-02-05 02:45:57 -05:00
public final SimilarityMetric SubstringFields = new SubstringMetric ( ) {
2013-09-06 03:55:13 -04:00
2011-10-28 02:28:19 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
2011-11-08 13:26:54 -05:00
String [ ] f1 = normalize ( fields ( o1 ) ) ;
String [ ] f2 = normalize ( fields ( o2 ) ) ;
2013-09-06 03:55:13 -04:00
2011-10-28 02:28:19 -04:00
// match all fields and average similarity
2016-09-26 04:22:34 -04:00
double sum = 0 ;
2016-09-26 06:59:17 -04:00
for ( int i = 0 ; i < f1 . length ; i + + ) {
for ( int j = 0 ; j < f2 . length ; j + + ) {
float f = super . getSimilarity ( f1 [ i ] , f2 [ j ] ) ;
2016-09-26 04:22:34 -04:00
if ( f > 0 ) {
// 2-sqrt(x) from 0 to 1
2016-09-26 06:59:17 -04:00
double multiplier = 2 - Math . sqrt ( ( double ) ( i + j ) / ( f1 . length + f2 . length ) ) ;
2016-09-26 04:22:34 -04:00
// bonus points for primary matches (e.g. primary title matches filename > alias title matches folder path)
sum + = f * multiplier ;
}
2011-10-28 02:28:19 -04:00
}
}
sum / = f1 . length * f2 . length ;
2013-09-06 03:55:13 -04:00
2016-09-26 06:59:17 -04:00
return sum > = 0 . 9 ? 1 : sum > = 0 . 1 ? 0 . 5f : 0 ;
2011-10-28 02:28:19 -04:00
}
2013-09-06 03:55:13 -04:00
2011-11-08 13:26:54 -05:00
protected String [ ] normalize ( Object [ ] objects ) {
2016-03-13 09:39:41 -04:00
// normalize objects (and make sure to keep word boundaries)
2019-02-05 02:45:57 -05:00
return stream ( objects ) . map ( EpisodeMetrics . this : : normalizeObject ) . toArray ( String [ ] : : new ) ;
2011-11-08 13:26:54 -05:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
protected final int MAX_FIELDS = 5 ;
2016-08-22 12:51:47 -04:00
2011-11-08 13:26:54 -05:00
protected Object [ ] fields ( Object object ) {
2011-10-28 02:28:19 -04:00
if ( object instanceof Episode ) {
2016-03-30 17:42:51 -04:00
Episode e = ( Episode ) object ;
2016-09-17 15:31:27 -04:00
2016-09-26 04:22:34 -04:00
Stream < String > primaryNames = Stream . of ( e . getSeriesName ( ) , e . getTitle ( ) ) ;
2016-10-20 15:48:33 -04:00
Stream < String > aliasNames = e . getSeriesInfo ( ) = = null ? Stream . empty ( ) : e . getSeriesInfo ( ) . getAliasNames ( ) . stream ( ) . limit ( MAX_FIELDS ) ;
2016-09-26 04:22:34 -04:00
Stream < String > names = Stream . concat ( primaryNames , aliasNames ) . filter ( s - > s ! = null & & s . length ( ) > 0 ) . map ( Normalization : : removeTrailingBrackets ) . distinct ( ) ;
2016-09-17 15:31:27 -04:00
return copyOf ( names . limit ( MAX_FIELDS ) . toArray ( ) , MAX_FIELDS ) ;
2011-10-28 02:28:19 -04:00
}
2013-09-06 03:55:13 -04:00
2011-10-28 02:28:19 -04:00
if ( object instanceof File ) {
2016-03-30 17:42:51 -04:00
File f = ( File ) object ;
2016-09-26 04:22:34 -04:00
return new Object [ ] { f , f . getParentFile ( ) . getPath ( ) } ;
2011-10-28 04:07:02 -04:00
}
2013-09-06 03:55:13 -04:00
2011-10-28 04:07:02 -04:00
if ( object instanceof Movie ) {
2016-03-30 17:42:51 -04:00
Movie m = ( Movie ) object ;
return new Object [ ] { m . getName ( ) , m . getYear ( ) } ;
2011-10-28 04:07:02 -04:00
}
2013-09-06 03:55:13 -04:00
2011-11-08 13:26:54 -05:00
return new Object [ ] { object } ;
2011-10-28 02:28:19 -04:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " SubstringFields " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2012-01-03 04:23:03 -05:00
// Match via common word sequence in episode name and file name
2019-02-05 02:45:57 -05:00
public final SimilarityMetric NameSubstringSequence = new SequenceMatchSimilarity ( ) {
2013-09-06 03:55:13 -04:00
2012-01-03 04:23:03 -05:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
2013-12-15 13:35:41 -05:00
String [ ] f1 = getNormalizedEffectiveIdentifiers ( o1 ) ;
String [ ] f2 = getNormalizedEffectiveIdentifiers ( o2 ) ;
// match all fields and average similarity
float max = 0 ;
for ( String s1 : f1 ) {
for ( String s2 : f2 ) {
2016-03-20 16:25:59 -04:00
max = Math . max ( super . getSimilarity ( s1 , s2 ) , max ) ;
2013-12-15 13:35:41 -05:00
}
}
2013-01-30 19:39:47 -05:00
// normalize absolute similarity to similarity rank (4 ranks in total),
2012-01-03 04:23:03 -05:00
// so we are less likely to fall for false positives in this pass, and move on to the next one
2016-03-20 16:25:59 -04:00
return ( float ) ( Math . floor ( max * 4 ) / 4 ) ;
2012-01-03 04:23:03 -05:00
}
2013-09-06 03:55:13 -04:00
2012-01-03 04:23:03 -05:00
@Override
protected String normalize ( Object object ) {
2013-12-15 13:35:41 -05:00
return object . toString ( ) ;
}
protected String [ ] getNormalizedEffectiveIdentifiers ( Object object ) {
2019-06-07 11:08:06 -04:00
return getEffectiveIdentifiers ( object ) . stream ( ) . map ( it - > {
return normalizeObject ( it ) ;
} ) . toArray ( String [ ] : : new ) ;
2013-12-15 13:35:41 -05:00
}
2019-06-07 11:08:06 -04:00
protected Collection < ? > getEffectiveIdentifiers ( Object object ) {
2013-02-15 04:50:23 -05:00
if ( object instanceof Episode ) {
2015-01-18 03:57:37 -05:00
return ( ( Episode ) object ) . getSeriesNames ( ) ;
2013-02-15 04:50:23 -05:00
} else if ( object instanceof Movie ) {
2013-12-15 13:35:41 -05:00
return ( ( Movie ) object ) . getEffectiveNames ( ) ;
2013-02-15 04:50:23 -05:00
} else if ( object instanceof File ) {
2013-12-15 13:35:41 -05:00
return listPathTail ( ( File ) object , 3 , true ) ;
2013-02-15 04:50:23 -05:00
}
2019-06-07 11:08:06 -04:00
return singleton ( object ) ;
2012-01-03 04:23:03 -05:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " NameSubstringSequence " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2013-02-15 04:50:23 -05:00
// Match by generic name similarity (round rank)
2019-02-05 02:45:57 -05:00
public final SimilarityMetric Name = new NameSimilarityMetric ( ) {
2013-09-06 03:55:13 -04:00
2009-07-26 12:54:24 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
2013-02-01 03:12:15 -05:00
// normalize absolute similarity to similarity rank (4 ranks in total),
2009-07-26 12:54:24 -04:00
// so we are less likely to fall for false positives in this pass, and move on to the next one
2016-03-20 16:25:59 -04:00
return ( float ) ( Math . floor ( super . getSimilarity ( o1 , o2 ) * 4 ) / 4 ) ;
2009-07-26 12:54:24 -04:00
}
2013-09-06 03:55:13 -04:00
2009-07-26 12:54:24 -04:00
@Override
protected String normalize ( Object object ) {
// simplify file name, if possible
2011-11-08 13:26:54 -05:00
return normalizeObject ( object ) ;
2009-07-26 12:54:24 -04:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " Name " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2013-02-15 04:50:23 -05:00
// Match by generic name similarity (absolute)
2019-02-05 02:45:57 -05:00
public final SimilarityMetric SeriesName = new NameSimilarityMetric ( ) {
2013-09-06 03:55:13 -04:00
2016-02-10 13:32:39 -05:00
private final SeriesNameMatcher seriesNameMatcher = getSeriesNameMatcher ( false ) ;
2013-09-06 03:55:13 -04:00
2013-04-02 11:34:25 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
2013-12-27 17:49:56 -05:00
String [ ] f1 = getNormalizedEffectiveIdentifiers ( o1 ) ;
String [ ] f2 = getNormalizedEffectiveIdentifiers ( o2 ) ;
2013-09-06 03:55:13 -04:00
2013-12-27 17:49:56 -05:00
// match all fields and average similarity
float max = 0 ;
for ( String s1 : f1 ) {
for ( String s2 : f2 ) {
2016-03-20 16:25:59 -04:00
max = Math . max ( super . getSimilarity ( s1 , s2 ) , max ) ;
2013-12-27 17:49:56 -05:00
}
}
// normalize absolute similarity to similarity rank (4 ranks in total),
// so we are less likely to fall for false positives in this pass, and move on to the next one
2016-03-20 16:25:59 -04:00
return ( float ) ( Math . floor ( max * 4 ) / 4 ) ;
2013-12-27 17:49:56 -05:00
}
2013-09-06 03:55:13 -04:00
2013-02-01 03:12:15 -05:00
@Override
protected String normalize ( Object object ) {
2013-04-02 11:34:25 -04:00
return object . toString ( ) ;
2013-12-27 17:49:56 -05:00
}
protected String [ ] getNormalizedEffectiveIdentifiers ( Object object ) {
2019-02-05 02:45:57 -05:00
return getEffectiveIdentifiers ( object ) . stream ( ) . map ( EpisodeMetrics . this : : normalizeObject ) . toArray ( String [ ] : : new ) ;
2013-12-27 17:49:56 -05:00
}
protected List < ? > getEffectiveIdentifiers ( Object object ) {
2013-04-02 11:34:25 -04:00
if ( object instanceof Episode ) {
2016-03-29 04:00:09 -04:00
Episode episode = ( Episode ) object ;
// strip release info from known series name to make sure it matches the stripped filename
return stripReleaseInfo ( episode . getSeriesNames ( ) , true ) ;
2013-04-02 11:34:25 -04:00
} else if ( object instanceof File ) {
2014-06-28 06:00:21 -04:00
File file = ( File ) object ;
// guess potential series names from path
2016-03-29 04:00:09 -04:00
return listPathTail ( file , 3 , true ) . stream ( ) . map ( f - > {
2013-12-27 17:49:56 -05:00
String fn = getName ( f ) ;
String sn = seriesNameMatcher . matchByEpisodeIdentifier ( fn ) ;
2016-03-29 04:00:09 -04:00
return sn ! = null ? sn : fn ;
} ) . collect ( collectingAndThen ( toList ( ) , v - > stripReleaseInfo ( v , true ) ) ) ;
2013-12-27 17:49:56 -05:00
}
2013-12-15 13:35:41 -05:00
2013-12-27 17:49:56 -05:00
return emptyList ( ) ;
2013-12-15 13:35:41 -05:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " SeriesName " ;
}
2019-02-05 02:45:57 -05:00
} ;
public final SimilarityMetric SeriesNameBalancer = new MetricCascade ( NameSubstringSequence , Name , SeriesName ) ;
2014-07-14 09:31:12 -04:00
2013-04-02 11:34:25 -04:00
// Match by generic name similarity (absolute)
2019-02-05 02:45:57 -05:00
public final SimilarityMetric FilePath = new NameSimilarityMetric ( ) {
2013-09-06 03:55:13 -04:00
2013-04-02 11:34:25 -04:00
@Override
protected String normalize ( Object object ) {
if ( object instanceof File ) {
object = normalizePathSeparators ( getRelativePathTail ( ( File ) object , 3 ) . getPath ( ) ) ;
}
return normalizeObject ( object . toString ( ) ) ; // simplify file name, if possible
}
2013-09-06 03:55:13 -04:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " FilePath " ;
}
2019-02-05 02:45:57 -05:00
} ;
public final SimilarityMetric FilePathBalancer = new NameSimilarityMetric ( ) {
2016-01-08 07:26:39 -05:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
String s1 = normalizeObject ( o1 ) ;
String s2 = normalizeObject ( o2 ) ;
2016-01-08 07:26:42 -05:00
s1 = stripReleaseInfo ( s1 , false ) ;
s2 = stripReleaseInfo ( s2 , false ) ;
2016-01-08 07:26:39 -05:00
2016-03-20 16:25:59 -04:00
int length = Math . min ( s1 . length ( ) , s2 . length ( ) ) ;
2016-01-08 07:26:39 -05:00
s1 = s1 . substring ( 0 , length ) ;
s2 = s2 . substring ( 0 , length ) ;
2016-03-20 16:25:59 -04:00
return ( float ) ( Math . floor ( super . getSimilarity ( s1 , s2 ) * 4 ) / 4 ) ;
2016-01-08 07:26:39 -05:00
} ;
@Override
protected String normalize ( Object object ) {
return object . toString ( ) ;
}
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " FilePathBalancer " ;
}
2019-02-05 02:45:57 -05:00
} ;
public final SimilarityMetric NumericSequence = new SequenceMatchSimilarity ( ) {
2013-09-06 03:55:13 -04:00
2013-04-06 14:37:46 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
float lowerBound = super . getSimilarity ( normalize ( o1 , true ) , normalize ( o2 , true ) ) ;
float upperBound = super . getSimilarity ( normalize ( o1 , false ) , normalize ( o2 , false ) ) ;
2013-09-06 03:55:13 -04:00
2016-03-20 16:25:59 -04:00
return Math . max ( lowerBound , upperBound ) ;
2013-04-06 14:37:46 -04:00
} ;
2013-09-06 03:55:13 -04:00
2013-02-15 04:50:23 -05:00
@Override
protected String normalize ( Object object ) {
2013-04-06 14:37:46 -04:00
return object . toString ( ) ;
} ;
2013-09-06 03:55:13 -04:00
2013-04-06 14:37:46 -04:00
protected String normalize ( Object object , boolean numbersOnly ) {
2013-02-15 04:50:23 -05:00
if ( object instanceof Episode ) {
2013-03-27 05:05:52 -04:00
Episode e = ( Episode ) object ;
2013-04-06 14:37:46 -04:00
if ( numbersOnly ) {
object = EpisodeFormat . SeasonEpisode . formatSxE ( e ) ;
} else {
object = String . format ( " %s %s " , e . getSeriesName ( ) , EpisodeFormat . SeasonEpisode . formatSxE ( e ) ) ;
}
2013-02-15 04:50:23 -05:00
} else if ( object instanceof Movie ) {
2013-04-06 14:37:46 -04:00
Movie m = ( Movie ) object ;
if ( numbersOnly ) {
object = m . getYear ( ) ;
} else {
object = String . format ( " %s %s " , m . getName ( ) , m . getYear ( ) ) ;
}
2013-02-15 04:50:23 -05:00
}
2013-09-06 03:55:13 -04:00
2013-02-15 04:50:23 -05:00
// simplify file name if possible and extract numbers
2016-01-09 23:54:35 -05:00
List < Integer > numbers = matchIntegers ( normalizeObject ( object ) ) ;
2015-05-25 06:37:42 -04:00
return join ( numbers , " " ) ;
2013-02-01 03:12:15 -05:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " NumericSequence " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2009-07-26 12:54:24 -04:00
// Match by generic numeric similarity
2019-02-05 02:45:57 -05:00
public final SimilarityMetric Numeric = new NumericSimilarityMetric ( ) {
2013-09-06 03:55:13 -04:00
2012-07-21 11:47:49 -04:00
@Override
2011-12-07 02:08:04 -05:00
public float getSimilarity ( Object o1 , Object o2 ) {
String [ ] f1 = fields ( o1 ) ;
String [ ] f2 = fields ( o2 ) ;
2013-09-06 03:55:13 -04:00
2011-12-07 02:08:04 -05:00
// match all fields and average similarity
2013-03-04 02:35:20 -05:00
float max = 0 ;
2011-12-07 02:08:04 -05:00
for ( String s1 : f1 ) {
for ( String s2 : f2 ) {
2013-10-14 23:22:47 -04:00
if ( s1 ! = null & & s2 ! = null ) {
2016-03-20 16:25:59 -04:00
max = Math . max ( super . getSimilarity ( s1 , s2 ) , max ) ;
2016-03-13 13:35:31 -04:00
if ( max > = 1 ) {
return max ;
}
2013-10-14 23:22:47 -04:00
}
2011-12-07 02:08:04 -05:00
}
}
2013-03-04 02:35:20 -05:00
return max ;
2011-12-07 02:08:04 -05:00
}
2013-09-06 03:55:13 -04:00
2011-12-07 02:08:04 -05:00
protected String [ ] fields ( Object object ) {
if ( object instanceof Episode ) {
Episode episode = ( Episode ) object ;
2016-03-13 13:35:31 -04:00
String [ ] f = new String [ 3 ] ;
2013-10-14 23:22:47 -04:00
f [ 0 ] = episode . getSeriesName ( ) ;
2016-03-13 13:35:31 -04:00
f [ 1 ] = episode . getSpecial ( ) = = null ? EpisodeFormat . SeasonEpisode . formatSxE ( episode ) : episode . getSpecial ( ) . toString ( ) ;
2013-10-14 23:22:47 -04:00
f [ 2 ] = episode . getAbsolute ( ) = = null ? null : episode . getAbsolute ( ) . toString ( ) ;
return f ;
2011-12-07 02:08:04 -05:00
}
2013-09-06 03:55:13 -04:00
2011-12-07 02:08:04 -05:00
if ( object instanceof Movie ) {
Movie movie = ( Movie ) object ;
2013-03-27 05:05:52 -04:00
return new String [ ] { movie . getName ( ) , String . valueOf ( movie . getYear ( ) ) } ;
2011-12-07 02:08:04 -05:00
}
2013-09-06 03:55:13 -04:00
2016-03-13 13:35:31 -04:00
return new String [ ] { normalizeObject ( object ) } ;
2009-07-26 12:54:24 -04:00
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " Numeric " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2016-03-13 13:35:34 -04:00
// Prioritize proper episodes over specials
2019-02-05 02:45:57 -05:00
public final SimilarityMetric SpecialNumber = new SimilarityMetric ( ) {
2016-03-13 13:35:34 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
return getSpecialFactor ( o1 ) + getSpecialFactor ( o2 ) ;
}
public int getSpecialFactor ( Object object ) {
if ( object instanceof Episode ) {
Episode episode = ( Episode ) object ;
return episode . getSpecial ( ) ! = null ? - 1 : 1 ;
}
return 0 ;
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " SpecialNumber " ;
}
2019-02-05 02:45:57 -05:00
} ;
2016-03-13 13:35:34 -04:00
2011-11-22 11:08:36 -05:00
// Match by file length (only works when matching torrents or files)
2019-02-05 02:45:57 -05:00
public final SimilarityMetric FileSize = new FileSizeMetric ( ) {
2013-09-06 03:55:13 -04:00
2011-11-22 11:08:36 -05:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
// order of arguments is logically irrelevant, but we might be able to save us a call to File.length() which is quite costly
return o1 instanceof File ? super . getSimilarity ( o2 , o1 ) : super . getSimilarity ( o1 , o2 ) ;
}
2013-09-06 03:55:13 -04:00
2011-11-22 11:08:36 -05:00
@Override
protected long getLength ( Object object ) {
2011-11-24 12:27:39 -05:00
if ( object instanceof FileInfo ) {
return ( ( FileInfo ) object ) . getLength ( ) ;
2011-11-22 11:08:36 -05:00
}
return super . getLength ( object ) ;
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " FileSize " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2011-11-24 12:27:39 -05:00
// Match by common words at the beginning of both files
2019-02-05 02:45:57 -05:00
public final SimilarityMetric FileName = new FileNameMetric ( ) {
2013-09-06 03:55:13 -04:00
2011-11-24 12:27:39 -05:00
@Override
protected String getFileName ( Object object ) {
if ( object instanceof File | | object instanceof FileInfo ) {
return normalizeObject ( object ) ;
}
return null ;
}
2019-02-05 02:45:57 -05:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " FileName " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2012-10-09 09:30:32 -04:00
// Match by file last modified and episode release dates
2019-05-27 07:33:20 -04:00
public final TimeStampMetric TimeStamp = new TimeStampMetric ( 10 , ChronoUnit . YEARS ) {
private final Map < File , Long > cache = synchronizedMap ( new HashMap < > ( ) ) ;
2013-09-06 03:55:13 -04:00
2012-10-09 09:30:32 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
2016-08-11 08:27:18 -04:00
// adjust differentiation accuracy to about 2.5 years
2013-04-02 12:41:22 -04:00
float f = super . getSimilarity ( o1 , o2 ) ;
2016-08-11 08:27:18 -04:00
return f > = 0 . 75 ? 1 : f > = 0 ? 0 : - 1 ;
2012-10-09 09:30:32 -04:00
}
2013-09-06 03:55:13 -04:00
2016-08-09 17:50:54 -04:00
private long getTimeStamp ( SimpleDate date ) {
// some episodes may not have a defined airdate
if ( date ! = null ) {
Instant t = date . toInstant ( ) ;
if ( t . isBefore ( Instant . now ( ) ) ) {
return t . toEpochMilli ( ) ;
}
}
2013-09-06 03:55:13 -04:00
2016-08-09 17:50:54 -04:00
// big penalty for episodes not yet aired
return - 1 ;
}
2013-09-06 03:55:13 -04:00
2016-08-09 17:50:54 -04:00
private long getTimeStamp ( File file ) {
2019-05-27 07:33:20 -04:00
return cache . computeIfAbsent ( file , f - > {
2019-06-15 04:14:22 -04:00
if ( MediaCharacteristicsParser . DEFAULT . acceptVideoFile ( f ) ) {
2019-05-27 07:33:20 -04:00
try ( MediaCharacteristics mi = MediaCharacteristicsParser . DEFAULT . open ( file ) ) {
Instant t = mi . getCreationTime ( ) ;
if ( t ! = null ) {
return t . toEpochMilli ( ) ;
}
} catch ( Exception e ) {
debug . warning ( " Failed to read media encoding date: " + e . getMessage ( ) ) ;
2018-12-06 02:56:26 -05:00
}
2012-10-09 11:00:21 -04:00
}
2016-08-09 17:50:54 -04:00
2019-05-27 07:33:20 -04:00
return super . getTimeStamp ( file ) ; // default to file creation date
} ) ;
2016-08-09 17:50:54 -04:00
}
@Override
public long getTimeStamp ( Object object ) {
if ( object instanceof Episode ) {
Episode e = ( Episode ) object ;
return getTimeStamp ( e . getAirdate ( ) ) ;
} else if ( object instanceof Movie ) {
Movie m = ( Movie ) object ;
return getTimeStamp ( new SimpleDate ( m . getYear ( ) , 1 , 1 ) ) ;
2016-08-09 15:11:06 -04:00
} else if ( object instanceof File ) {
File file = ( File ) object ;
2016-08-09 17:50:54 -04:00
return getTimeStamp ( file ) ;
2012-10-09 09:30:32 -04:00
}
2013-09-06 03:55:13 -04:00
2016-08-09 17:50:54 -04:00
return - 1 ;
2012-10-09 09:30:32 -04:00
}
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " TimeStamp " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2019-05-27 07:33:20 -04:00
// Match by recently aired status
public final SimilarityMetric RecentlyAired = new TimeStampMetric ( 3 , ChronoUnit . DAYS ) {
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
return super . getSimilarity ( o1 , o2 ) > 0 ? 1 : 0 ;
}
@Override
public long getTimeStamp ( Object object ) {
return object instanceof Episode | | object instanceof File ? TimeStamp . getTimeStamp ( object ) : - 1 ;
}
@Override
public String toString ( ) {
return " RecentlyAired " ;
}
} ;
2019-02-05 02:45:57 -05:00
public final SimilarityMetric SeriesRating = new SimilarityMetric ( ) {
2013-09-06 03:55:13 -04:00
2013-04-06 13:49:27 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
2016-02-22 16:29:26 -05:00
float r1 = getScore ( o1 ) ;
float r2 = getScore ( o2 ) ;
2014-03-22 05:51:43 -04:00
if ( r1 < 0 | | r2 < 0 )
return - 1 ;
2016-03-20 16:25:59 -04:00
return Math . max ( r1 , r2 ) ;
2013-04-06 13:49:27 -04:00
}
2013-09-06 03:55:13 -04:00
2016-02-22 16:29:26 -05:00
public float getScore ( Object object ) {
2014-12-10 13:53:58 -05:00
if ( object instanceof Episode ) {
SeriesInfo seriesInfo = ( ( Episode ) object ) . getSeriesInfo ( ) ;
if ( seriesInfo ! = null & & seriesInfo . getRating ( ) ! = null & & seriesInfo . getRatingCount ( ) ! = null ) {
2015-09-09 05:51:11 -04:00
if ( seriesInfo . getRatingCount ( ) > = 20 ) {
2016-03-20 16:25:59 -04:00
return ( float ) Math . floor ( seriesInfo . getRating ( ) / 3 ) ; // BOOST POPULAR SHOWS and PUT INTO 3 GROUPS
2014-12-10 13:53:58 -05:00
}
if ( seriesInfo . getRatingCount ( ) > = 1 ) {
return 0 ; // PENALIZE SHOWS WITH FEW RATINGS
}
return - 1 ; // BIG PENALTY FOR SHOWS WITH 0 RATINGS
2013-04-06 13:49:27 -04:00
}
}
return 0 ;
}
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " SeriesRating " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
public final SimilarityMetric VoteRate = new SimilarityMetric ( ) {
2016-02-22 16:29:26 -05:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
float r1 = getScore ( o1 ) ;
float r2 = getScore ( o2 ) ;
2016-03-20 16:25:59 -04:00
return Math . max ( r1 , r2 ) > = 0 . 1 ? 1 : 0 ;
2016-02-22 16:29:26 -05:00
}
public float getScore ( Object object ) {
if ( object instanceof Episode ) {
SeriesInfo seriesInfo = ( ( Episode ) object ) . getSeriesInfo ( ) ;
if ( seriesInfo ! = null & & seriesInfo . getRating ( ) ! = null & & seriesInfo . getRatingCount ( ) ! = null & & seriesInfo . getStartDate ( ) ! = null ) {
long days = ChronoUnit . DAYS . between ( seriesInfo . getStartDate ( ) . toLocalDate ( ) , LocalDate . now ( ) ) ;
if ( days > 0 ) {
return ( float ) ( ( seriesInfo . getRatingCount ( ) . doubleValue ( ) / days ) * seriesInfo . getRating ( ) ) ;
}
}
}
return 0 ;
}
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " VoteRate " ;
}
2019-02-05 02:45:57 -05:00
} ;
2016-02-22 16:29:26 -05:00
2013-10-29 14:34:39 -04:00
// Match by (region) or (year) hints
2019-02-05 02:45:57 -05:00
public final SimilarityMetric RegionHint = new SimilarityMetric ( ) {
2013-10-29 14:34:39 -04:00
2016-02-10 13:32:39 -05:00
private final Pattern hint = compile ( " [(]( \\ p{Alpha}+| \\ p{Digit}+)[)]$ " ) ;
private final SeriesNameMatcher seriesNameMatcher = getSeriesNameMatcher ( true ) ;
2013-10-29 14:34:39 -04:00
@Override
public float getSimilarity ( Object o1 , Object o2 ) {
Set < String > h1 = getHint ( o1 ) ;
Set < String > h2 = getHint ( o2 ) ;
return h1 . isEmpty ( ) | | h2 . isEmpty ( ) ? 0 : h1 . containsAll ( h2 ) | | h2 . containsAll ( h1 ) ? 1 : 0 ;
}
public Set < String > getHint ( Object o ) {
if ( o instanceof Episode ) {
2015-01-18 03:57:37 -05:00
for ( String sn : ( ( Episode ) o ) . getSeriesNames ( ) ) {
Matcher m = hint . matcher ( sn ) ;
if ( m . find ( ) ) {
return singleton ( m . group ( 1 ) . trim ( ) . toLowerCase ( ) ) ;
}
2013-10-29 14:34:39 -04:00
}
} else if ( o instanceof File ) {
Set < String > h = new HashSet < String > ( ) ;
for ( File f : listPathTail ( ( File ) o , 3 , true ) ) {
// try to focus on series name
2016-04-05 14:06:02 -04:00
String fn = f . getName ( ) ;
String sn = seriesNameMatcher . matchByEpisodeIdentifier ( fn ) ;
String [ ] tokens = PUNCTUATION_OR_SPACE . split ( sn ! = null ? sn : fn ) ;
2013-10-29 14:34:39 -04:00
for ( String s : tokens ) {
if ( s . length ( ) > 0 ) {
h . add ( s . trim ( ) . toLowerCase ( ) ) ;
}
}
}
return h ;
}
return emptySet ( ) ;
}
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " RegionHint " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-10-29 14:34:39 -04:00
2012-10-24 11:20:47 -04:00
// Match by stored MetaAttributes if possible
2019-02-05 02:45:57 -05:00
public final SimilarityMetric MetaAttributes = new CrossPropertyMetric ( ) {
2013-09-06 03:55:13 -04:00
2012-10-24 11:20:47 -04:00
@Override
protected Map < String , Object > getProperties ( Object object ) {
// Episode / Movie objects
if ( object instanceof Episode | | object instanceof Movie ) {
return super . getProperties ( object ) ;
}
2013-09-06 03:55:13 -04:00
// deserialize MetaAttributes if enabled and available
2014-06-24 06:59:00 -04:00
if ( object instanceof File ) {
2016-03-27 12:56:54 -04:00
Object metaObject = xattr . getMetaInfo ( ( File ) object ) ;
2014-06-24 06:59:00 -04:00
if ( metaObject ! = null ) {
return super . getProperties ( metaObject ) ;
2012-10-24 11:20:47 -04:00
}
}
2013-09-06 03:55:13 -04:00
2012-10-24 11:20:47 -04:00
// ignore everything else
return emptyMap ( ) ;
2014-03-22 05:51:43 -04:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 03:31:44 -05:00
@Override
public String toString ( ) {
return " MetaAttributes " ;
}
2019-02-05 02:45:57 -05:00
} ;
2013-09-06 03:55:13 -04:00
2019-02-05 03:10:36 -05:00
protected final Map < Object , String > transformCache = synchronizedMap ( new HashMap < > ( 64 , 4 ) ) ;
2019-02-02 11:04:39 -05:00
2019-02-05 02:45:57 -05:00
protected final Transliterator transliterator = Transliterator . getInstance ( " Any-Latin;Latin-ASCII;[:Diacritic:]remove " ) ;
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
protected String normalizeObject ( Object object ) {
if ( object = = null ) {
return " " ;
2019-02-05 01:28:19 -05:00
}
2019-02-05 02:45:57 -05:00
return transformCache . computeIfAbsent ( object , o - > {
// 1. convert to string
// 2. remove checksums, any [...] or (...)
// 3. remove obvious release info
// 4. apply transliterator
// 5. remove or normalize special characters
2019-02-05 03:10:36 -05:00
return normalizePunctuation ( transliterator . transform ( stripFormatInfo ( removeEmbeddedChecksum ( normalizeFileName ( o ) ) ) ) ) . toLowerCase ( ) ;
2019-02-05 02:45:57 -05:00
} ) ;
2017-02-18 11:41:36 -05:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
protected String normalizeFileName ( Object object ) {
2017-02-18 11:41:36 -05:00
if ( object instanceof File ) {
return getName ( ( File ) object ) ;
} else if ( object instanceof FileInfo ) {
return ( ( FileInfo ) object ) . getName ( ) ;
}
return object . toString ( ) ;
2009-07-26 12:54:24 -04:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
public SimilarityMetric [ ] matchSequence ( ) {
2011-12-25 10:47:19 -05:00
// 1 pass: divide by file length (only works for matching torrent entries or files)
// 2-3 pass: divide by title or season / episode numbers
// 4 pass: divide by folder / file name and show name / episode title
// 5 pass: divide by name (rounded into n levels)
// 6 pass: divide by generic numeric similarity
2012-10-09 09:30:32 -04:00
// 7 pass: prefer episodes that were aired closer to the last modified date of the file
// 8 pass: resolve remaining collisions via absolute string similarity
2019-05-27 07:33:20 -04:00
return new SimilarityMetric [ ] { EpisodeFunnel , EpisodeBalancer , AirDate , MetaAttributes , SubstringFields , SeriesNameBalancer , SeriesName , RegionHint , SpecialNumber , Numeric , NumericSequence , SeriesRating , VoteRate , TimeStamp , RecentlyAired , FilePathBalancer , FilePath } ;
2019-02-05 02:45:57 -05:00
}
public SimilarityMetric [ ] matchFileSequence ( ) {
2019-05-27 07:33:20 -04:00
return new SimilarityMetric [ ] { FileSize , new MetricCascade ( FileName , EpisodeFunnel ) , EpisodeBalancer , AirDate , MetaAttributes , SubstringFields , SeriesNameBalancer , SeriesName , RegionHint , SpecialNumber , Numeric , NumericSequence , SeriesRating , VoteRate , TimeStamp , RecentlyAired , FilePathBalancer , FilePath } ;
2009-07-26 12:54:24 -04:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
public SimilarityMetric numbers ( ) {
return EpisodeIdentifier ;
}
public SimilarityMetric verification ( ) {
2014-01-02 06:28:28 -05:00
return new MetricCascade ( FileName , SeasonEpisode , AirDate , Title , Name ) ;
2011-11-27 09:35:53 -05:00
}
2013-09-06 03:55:13 -04:00
2019-02-05 02:45:57 -05:00
public SimilarityMetric sanity ( ) {
return new MetricCascade ( new MetricMin ( FileSize , 0 ) , FileName , EpisodeIdentifier ) ;
}
2009-07-26 12:54:24 -04:00
}