2018-09-12 17:26:21 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text.RegularExpressions ;
2019-01-13 19:17:29 +00:00
using Emby.Naming.Common ;
2019-12-06 19:40:06 +00:00
using MediaBrowser.Model.Entities ;
2019-01-13 19:17:29 +00:00
using MediaBrowser.Model.IO ;
2018-09-12 17:26:21 +00:00
namespace Emby.Naming.Video
{
2020-11-10 18:23:10 +00:00
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
2018-09-12 17:26:21 +00:00
public class VideoListResolver
{
private readonly NamingOptions _options ;
2020-11-10 18:23:10 +00: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 17:26:21 +00:00
public VideoListResolver ( NamingOptions options )
{
_options = options ;
}
2020-11-10 18:23:10 +00: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>
2020-11-18 13:23:45 +00:00
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
2018-09-12 17:26:21 +00: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 09:47:31 +00:00
. OfType < VideoFileInfo > ( )
2018-09-12 17:26:21 +00: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 19:40:06 +00:00
. Where ( i = > i . ExtraType = = null )
2020-03-25 16:53:03 +00:00
. Select ( i = > new FileSystemMetadata { FullName = i . Path , IsDirectory = i . IsDirectory } ) ;
2018-09-12 17:26:21 +00:00
var stackResult = new StackResolver ( _options )
2020-01-22 21:18:56 +00:00
. Resolve ( nonExtras ) . ToList ( ) ;
2018-09-12 17:26:21 +00:00
var remainingFiles = videoInfos
2020-11-01 09:47:31 +00:00
. Where ( i = > ! stackResult . Any ( s = > i . Path ! = null & & s . ContainsFile ( i . Path , i . IsDirectory ) ) )
2018-09-12 17:26:21 +00:00
. ToList ( ) ;
var list = new List < VideoInfo > ( ) ;
2020-01-22 21:18:56 +00:00
foreach ( var stack in stackResult )
2018-09-12 17:26:21 +00:00
{
2020-01-22 21:18:56 +00:00
var info = new VideoInfo ( stack . Name )
2018-09-12 17:26:21 +00:00
{
2020-11-01 09:47:31 +00:00
Files = stack . Files . Select ( i = > videoResolver . Resolve ( i , stack . IsDirectoryStack ) )
. OfType < VideoFileInfo > ( )
. ToList ( )
2018-09-12 17:26:21 +00:00
} ;
2019-05-10 18:37:42 +00:00
info . Year = info . Files [ 0 ] . Year ;
2018-09-12 17:26:21 +00:00
2020-03-25 16:53:03 +00:00
var extraBaseNames = new List < string > { stack . Name , Path . GetFileNameWithoutExtension ( stack . Files [ 0 ] ) } ;
2018-09-12 17:26:21 +00: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 19:40:06 +00:00
. Where ( i = > i . ExtraType = = null )
2018-09-12 17:26:21 +00:00
. ToList ( ) ;
foreach ( var media in standaloneMedia )
{
2020-03-25 16:53:03 +00:00
var info = new VideoInfo ( media . Name ) { Files = new List < VideoFileInfo > { media } } ;
2018-09-12 17:26:21 +00:00
2019-05-10 18:37:42 +00:00
info . Year = info . Files [ 0 ] . Year ;
2018-09-12 17:26:21 +00: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 18:37:42 +00:00
var folderName = Path . GetFileName ( parentPath ) ;
2018-09-12 17:26:21 +00:00
if ( ! string . IsNullOrEmpty ( folderName ) )
{
var extras = GetExtras ( remainingFiles , new List < string > { folderName } ) ;
remainingFiles = remainingFiles
. Except ( extras )
. ToList ( ) ;
2020-01-22 21:18:56 +00:00
extras . AddRange ( info . Extras ) ;
info . Extras = extras ;
2018-09-12 17:26:21 +00: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 21:18:56 +00:00
extrasByFileName . AddRange ( info . Extras ) ;
info . Extras = extrasByFileName ;
2018-09-12 17:26:21 +00:00
}
// If there's only one video, accept all trailers
2020-11-12 11:54:55 +00:00
// Be lenient because people use all kinds of mishmash conventions with trailers.
2018-09-12 17:26:21 +00:00
if ( list . Count = = 1 )
{
var trailers = remainingFiles
2019-12-06 19:40:06 +00:00
. Where ( i = > i . ExtraType = = ExtraType . Trailer )
2018-09-12 17:26:21 +00:00
. ToList ( ) ;
2020-01-22 21:18:56 +00:00
trailers . AddRange ( list [ 0 ] . Extras ) ;
list [ 0 ] . Extras = trailers ;
2018-09-12 17:26:21 +00:00
remainingFiles = remainingFiles
. Except ( trailers )
. ToList ( ) ;
}
// Whatever files are left, just add them
2020-01-22 21:18:56 +00:00
list . AddRange ( remainingFiles . Select ( i = > new VideoInfo ( i . Name )
2018-09-12 17:26:21 +00:00
{
2020-03-25 20:33:44 +00:00
Files = new List < VideoFileInfo > { i } ,
Year = i . Year
2018-09-12 17:26:21 +00:00
} ) ) ;
2020-01-22 21:18:56 +00:00
return list ;
2018-09-12 17:26:21 +00:00
}
private IEnumerable < VideoInfo > GetVideosGroupedByVersion ( List < VideoInfo > videos )
{
if ( videos . Count = = 0 )
{
return videos ;
}
2019-02-19 16:39:47 +00:00
var list = new List < VideoInfo > ( ) ;
2019-02-19 16:34:43 +00:00
var folderName = Path . GetFileName ( Path . GetDirectoryName ( videos [ 0 ] . Files [ 0 ] . Path ) ) ;
2019-05-10 18:37:42 +00:00
if ( ! string . IsNullOrEmpty ( folderName )
& & folderName . Length > 1
& & videos . All ( i = > i . Files . Count = = 1
2021-01-21 14:45:54 +00:00
& & IsEligibleForMultiVersion ( folderName , i . Files [ 0 ] . Path ) )
& & HaveSameYear ( videos ) )
2018-09-12 17:26:21 +00:00
{
2019-05-10 18:37:42 +00:00
var ordered = videos . OrderBy ( i = > i . Name ) . ToList ( ) ;
2019-02-19 16:39:47 +00:00
2019-05-10 18:37:42 +00:00
list . Add ( ordered [ 0 ] ) ;
2019-02-19 16:39:47 +00:00
2020-01-22 21:18:56 +00: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 18:37:42 +00:00
list [ 0 ] . Name = folderName ;
2020-01-22 21:18:56 +00:00
var extras = ordered . Skip ( 1 ) . SelectMany ( i = > i . Extras ) . ToList ( ) ;
extras . AddRange ( list [ 0 ] . Extras ) ;
list [ 0 ] . Extras = extras ;
2019-02-19 16:39:47 +00:00
2019-05-10 18:37:42 +00:00
return list ;
2019-02-19 16:34:43 +00:00
}
return videos ;
2019-02-19 16:39:47 +00:00
}
private bool HaveSameYear ( List < VideoInfo > videos )
{
return videos . Select ( i = > i . Year ? ? - 1 ) . Distinct ( ) . Count ( ) < 2 ;
}
2021-01-21 14:45:54 +00:00
private bool IsEligibleForMultiVersion ( string folderName , string testFilePath )
2019-02-19 16:39:47 +00:00
{
2021-01-21 14:45:54 +00:00
string testFilename = Path . GetFileNameWithoutExtension ( testFilePath ) ;
2019-02-19 16:39:47 +00:00
if ( testFilename . StartsWith ( folderName , StringComparison . OrdinalIgnoreCase ) )
{
2021-03-10 09:47:35 +00:00
// Remove the folder name before cleaning as we don't care about cleaning that part
if ( folderName . Length < = testFilename . Length )
2020-11-05 13:51:27 +00:00
{
2021-03-10 09:47:35 +00:00
testFilename = testFilename . Substring ( folderName . Length ) . Trim ( ) ;
2020-11-05 13:51:27 +00:00
}
2020-11-22 21:39:34 +00:00
2021-03-10 09:47:35 +00:00
if ( CleanStringParser . TryClean ( testFilename , _options . CleanStringRegexes , out var cleanName ) )
2020-11-22 21:17:42 +00:00
{
2021-03-10 18:29:52 +00:00
testFilename = cleanName . Trim ( ) . ToString ( ) ;
2020-11-22 21:17:42 +00:00
}
2020-11-05 13:51:27 +00:00
2021-03-10 18:29:52 +00:00
// The CleanStringParser should have removed common keywords etc.
2019-05-10 18:37:42 +00:00
return string . IsNullOrEmpty ( testFilename )
2021-03-10 09:47:35 +00:00
| | testFilename [ 0 ] = = '-'
2021-03-10 18:29:52 +00:00
| | Regex . IsMatch ( testFilename , @"^\[([^]]*)\]" ) ;
2019-02-19 16:39:47 +00:00
}
return false ;
2018-09-12 17:26:21 +00: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-14 22:55:00 +00:00
. Where ( i = > i . ExtraType ! = null )
2020-03-25 16:53:03 +00:00
. Where ( i = > baseNames . Any ( b = >
i . FileNameWithoutExtension . StartsWith ( b , StringComparison . OrdinalIgnoreCase ) ) )
2018-09-12 17:26:21 +00:00
. ToList ( ) ;
}
}
}