2018-09-12 19:26:21 +02:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text.RegularExpressions ;
2019-01-13 20:17:29 +01:00
using Emby.Naming.Common ;
2019-12-06 20:40:06 +01:00
using MediaBrowser.Model.Entities ;
2019-01-13 20:17:29 +01:00
using MediaBrowser.Model.IO ;
2018-09-12 19:26:21 +02:00
namespace Emby.Naming.Video
{
2020-11-10 19:23:10 +01:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
2018-09-12 19:26:21 +02:00
public class VideoListResolver
{
private readonly NamingOptions _options ;
2020-11-10 19:23:10 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
2018-09-12 19:26:21 +02:00
public VideoListResolver ( NamingOptions options )
{
_options = options ;
}
2020-11-10 19:23:10 +01:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files togeather when related.</returns>
2018-09-12 19:26:21 +02:00
public IEnumerable < VideoInfo > Resolve ( List < FileSystemMetadata > files , bool supportMultiVersion = true )
{
var videoResolver = new VideoResolver ( _options ) ;
var videoInfos = files
. Select ( i = > videoResolver . Resolve ( i . FullName , i . IsDirectory ) )
2020-11-01 10:47:31 +01:00
// .Where(i => i != null)
. OfType < VideoFileInfo > ( )
2018-09-12 19:26:21 +02:00
. ToList ( ) ;
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
var nonExtras = videoInfos
2019-12-06 20:40:06 +01:00
. Where ( i = > i . ExtraType = = null )
2020-03-25 17:53:03 +01:00
. Select ( i = > new FileSystemMetadata { FullName = i . Path , IsDirectory = i . IsDirectory } ) ;
2018-09-12 19:26:21 +02:00
var stackResult = new StackResolver ( _options )
2020-01-22 22:18:56 +01:00
. Resolve ( nonExtras ) . ToList ( ) ;
2018-09-12 19:26:21 +02:00
var remainingFiles = videoInfos
2020-11-01 10:47:31 +01:00
. Where ( i = > ! stackResult . Any ( s = > i . Path ! = null & & s . ContainsFile ( i . Path , i . IsDirectory ) ) )
2018-09-12 19:26:21 +02:00
. ToList ( ) ;
var list = new List < VideoInfo > ( ) ;
2020-01-22 22:18:56 +01:00
foreach ( var stack in stackResult )
2018-09-12 19:26:21 +02:00
{
2020-01-22 22:18:56 +01:00
var info = new VideoInfo ( stack . Name )
2018-09-12 19:26:21 +02:00
{
2020-11-01 10:47:31 +01:00
Files = stack . Files . Select ( i = > videoResolver . Resolve ( i , stack . IsDirectoryStack ) )
. OfType < VideoFileInfo > ( )
. ToList ( )
2018-09-12 19:26:21 +02:00
} ;
2019-05-10 20:37:42 +02:00
info . Year = info . Files [ 0 ] . Year ;
2018-09-12 19:26:21 +02:00
2020-03-25 17:53:03 +01:00
var extraBaseNames = new List < string > { stack . Name , Path . GetFileNameWithoutExtension ( stack . Files [ 0 ] ) } ;
2018-09-12 19:26:21 +02:00
var extras = GetExtras ( remainingFiles , extraBaseNames ) ;
if ( extras . Count > 0 )
{
remainingFiles = remainingFiles
. Except ( extras )
. ToList ( ) ;
info . Extras = extras ;
}
list . Add ( info ) ;
}
var standaloneMedia = remainingFiles
2019-12-06 20:40:06 +01:00
. Where ( i = > i . ExtraType = = null )
2018-09-12 19:26:21 +02:00
. ToList ( ) ;
foreach ( var media in standaloneMedia )
{
2020-03-25 17:53:03 +01:00
var info = new VideoInfo ( media . Name ) { Files = new List < VideoFileInfo > { media } } ;
2018-09-12 19:26:21 +02:00
2019-05-10 20:37:42 +02:00
info . Year = info . Files [ 0 ] . Year ;
2018-09-12 19:26:21 +02:00
var extras = GetExtras ( remainingFiles , new List < string > { media . FileNameWithoutExtension } ) ;
remainingFiles = remainingFiles
. Except ( extras . Concat ( new [ ] { media } ) )
. ToList ( ) ;
info . Extras = extras ;
list . Add ( info ) ;
}
if ( supportMultiVersion )
{
list = GetVideosGroupedByVersion ( list )
. ToList ( ) ;
}
// If there's only one resolved video, use the folder name as well to find extras
if ( list . Count = = 1 )
{
var info = list [ 0 ] ;
var videoPath = list [ 0 ] . Files [ 0 ] . Path ;
var parentPath = Path . GetDirectoryName ( videoPath ) ;
if ( ! string . IsNullOrEmpty ( parentPath ) )
{
2019-05-10 20:37:42 +02:00
var folderName = Path . GetFileName ( parentPath ) ;
2018-09-12 19:26:21 +02:00
if ( ! string . IsNullOrEmpty ( folderName ) )
{
var extras = GetExtras ( remainingFiles , new List < string > { folderName } ) ;
remainingFiles = remainingFiles
. Except ( extras )
. ToList ( ) ;
2020-01-22 22:18:56 +01:00
extras . AddRange ( info . Extras ) ;
info . Extras = extras ;
2018-09-12 19:26:21 +02:00
}
}
// Add the extras that are just based on file name as well
var extrasByFileName = remainingFiles
. Where ( i = > i . ExtraRule ! = null & & i . ExtraRule . RuleType = = ExtraRuleType . Filename )
. ToList ( ) ;
remainingFiles = remainingFiles
. Except ( extrasByFileName )
. ToList ( ) ;
2020-01-22 22:18:56 +01:00
extrasByFileName . AddRange ( info . Extras ) ;
info . Extras = extrasByFileName ;
2018-09-12 19:26:21 +02:00
}
// If there's only one video, accept all trailers
2020-11-12 12:54:55 +01:00
// Be lenient because people use all kinds of mishmash conventions with trailers.
2018-09-12 19:26:21 +02:00
if ( list . Count = = 1 )
{
var trailers = remainingFiles
2019-12-06 20:40:06 +01:00
. Where ( i = > i . ExtraType = = ExtraType . Trailer )
2018-09-12 19:26:21 +02:00
. ToList ( ) ;
2020-01-22 22:18:56 +01:00
trailers . AddRange ( list [ 0 ] . Extras ) ;
list [ 0 ] . Extras = trailers ;
2018-09-12 19:26:21 +02:00
remainingFiles = remainingFiles
. Except ( trailers )
. ToList ( ) ;
}
// Whatever files are left, just add them
2020-01-22 22:18:56 +01:00
list . AddRange ( remainingFiles . Select ( i = > new VideoInfo ( i . Name )
2018-09-12 19:26:21 +02:00
{
2020-03-25 21:33:44 +01:00
Files = new List < VideoFileInfo > { i } ,
Year = i . Year
2018-09-12 19:26:21 +02:00
} ) ) ;
2020-01-22 22:18:56 +01:00
return list ;
2018-09-12 19:26:21 +02:00
}
private IEnumerable < VideoInfo > GetVideosGroupedByVersion ( List < VideoInfo > videos )
{
if ( videos . Count = = 0 )
{
return videos ;
}
2019-02-19 17:39:47 +01:00
var list = new List < VideoInfo > ( ) ;
2019-02-19 17:34:43 +01:00
var folderName = Path . GetFileName ( Path . GetDirectoryName ( videos [ 0 ] . Files [ 0 ] . Path ) ) ;
2019-05-10 20:37:42 +02:00
if ( ! string . IsNullOrEmpty ( folderName )
& & folderName . Length > 1
& & videos . All ( i = > i . Files . Count = = 1
2020-03-26 01:29:47 +01:00
& & IsEligibleForMultiVersion ( folderName , i . Files [ 0 ] . Path ) )
2019-05-10 20:37:42 +02:00
& & HaveSameYear ( videos ) )
2018-09-12 19:26:21 +02:00
{
2019-05-10 20:37:42 +02:00
var ordered = videos . OrderBy ( i = > i . Name ) . ToList ( ) ;
2019-02-19 17:39:47 +01:00
2019-05-10 20:37:42 +02:00
list . Add ( ordered [ 0 ] ) ;
2019-02-19 17:39:47 +01:00
2020-01-22 22:18:56 +01:00
var alternateVersionsLen = ordered . Count - 1 ;
var alternateVersions = new VideoFileInfo [ alternateVersionsLen ] ;
for ( int i = 0 ; i < alternateVersionsLen ; i + + )
{
alternateVersions [ i ] = ordered [ i + 1 ] . Files [ 0 ] ;
}
list [ 0 ] . AlternateVersions = alternateVersions ;
2019-05-10 20:37:42 +02:00
list [ 0 ] . Name = folderName ;
2020-01-22 22:18:56 +01:00
var extras = ordered . Skip ( 1 ) . SelectMany ( i = > i . Extras ) . ToList ( ) ;
extras . AddRange ( list [ 0 ] . Extras ) ;
list [ 0 ] . Extras = extras ;
2019-02-19 17:39:47 +01:00
2019-05-10 20:37:42 +02:00
return list ;
2019-02-19 17:34:43 +01:00
}
return videos ;
2019-02-19 17:39:47 +01:00
}
private bool HaveSameYear ( List < VideoInfo > videos )
{
return videos . Select ( i = > i . Year ? ? - 1 ) . Distinct ( ) . Count ( ) < 2 ;
}
2020-11-01 10:47:31 +01:00
private bool IsEligibleForMultiVersion ( string folderName , string? testFilename )
2019-02-19 17:39:47 +01:00
{
2019-02-19 19:33:41 +01:00
testFilename = Path . GetFileNameWithoutExtension ( testFilename ) ? ? string . Empty ;
2019-02-19 17:39:47 +01:00
if ( testFilename . StartsWith ( folderName , StringComparison . OrdinalIgnoreCase ) )
{
2020-11-05 14:51:27 +01:00
if ( CleanStringParser . TryClean ( testFilename , _options . CleanStringRegexes , out var cleanName ) )
{
testFilename = cleanName . ToString ( ) ;
}
2019-02-19 17:39:47 +01:00
testFilename = testFilename . Substring ( folderName . Length ) . Trim ( ) ;
2019-05-10 20:37:42 +02:00
return string . IsNullOrEmpty ( testFilename )
2020-11-12 13:16:33 +01:00
| | testFilename [ 0 ] . Equals ( '-' )
| | testFilename [ 0 ] . Equals ( '_' )
2020-03-26 01:29:47 +01:00
| | string . IsNullOrWhiteSpace ( Regex . Replace ( testFilename , @"\[([^]]*)\]" , string . Empty ) ) ;
2019-02-19 17:39:47 +01:00
}
return false ;
2018-09-12 19:26:21 +02:00
}
private List < VideoFileInfo > GetExtras ( IEnumerable < VideoFileInfo > remainingFiles , List < string > baseNames )
{
foreach ( var name in baseNames . ToList ( ) )
{
var trimmedName = name . TrimEnd ( ) . TrimEnd ( _options . VideoFlagDelimiters ) . TrimEnd ( ) ;
baseNames . Add ( trimmedName ) ;
}
return remainingFiles
2020-05-15 00:55:00 +02:00
. Where ( i = > i . ExtraType ! = null )
2020-03-25 17:53:03 +01:00
. Where ( i = > baseNames . Any ( b = >
i . FileNameWithoutExtension . StartsWith ( b , StringComparison . OrdinalIgnoreCase ) ) )
2018-09-12 19:26:21 +02:00
. ToList ( ) ;
}
}
}