using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
using Emby.Naming.Common ;
using MediaBrowser.Controller ;
using MediaBrowser.Controller.Configuration ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Entities.Movies ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.LiveTv ;
using MediaBrowser.Controller.MediaEncoding ;
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Globalization ;
using MediaBrowser.Model.MediaInfo ;
using MediaBrowser.Providers.MediaInfo ;
using Moq ;
using Xunit ;
namespace Jellyfin.Providers.Tests.MediaInfo ;
public class MediaInfoResolverTests
{
public const string VideoDirectoryPath = "Test Data/Video" ;
private const string VideoDirectoryRegex = @"Test Data[/\\]Video" ;
private const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000" ;
private const string MetadataDirectoryRegex = @"library.*" ;
private readonly ILocalizationManager _localizationManager ;
private readonly MediaInfoResolver _subtitleResolver ;
public MediaInfoResolverTests ( )
{
// prep BaseItem and Video for calls made that expect managers
Video . LiveTvManager = Mock . Of < ILiveTvManager > ( ) ;
var applicationPaths = new Mock < IServerApplicationPaths > ( ) . Object ;
var serverConfig = new Mock < IServerConfigurationManager > ( ) ;
serverConfig . Setup ( c = > c . ApplicationPaths )
. Returns ( applicationPaths ) ;
BaseItem . ConfigurationManager = serverConfig . Object ;
// build resolver to test with
var englishCultureDto = new CultureDto ( "English" , "English" , "en" , new [ ] { "eng" } ) ;
var localizationManager = new Mock < ILocalizationManager > ( MockBehavior . Loose ) ;
localizationManager . Setup ( lm = > lm . FindLanguageInfo ( It . IsRegex ( @"en.*" , RegexOptions . IgnoreCase ) ) )
. Returns ( englishCultureDto ) ;
_localizationManager = localizationManager . Object ;
var mediaEncoder = new Mock < IMediaEncoder > ( MockBehavior . Strict ) ;
mediaEncoder . Setup ( me = > me . GetMediaInfo ( It . IsAny < MediaInfoRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
. Returns < MediaInfoRequest , CancellationToken > ( ( _ , _ ) = > Task . FromResult ( new MediaBrowser . Model . MediaInfo . MediaInfo
{
MediaStreams = new List < MediaStream >
{
new ( )
}
} ) ) ;
_subtitleResolver = new SubtitleResolver ( _localizationManager , mediaEncoder . Object , new NamingOptions ( ) ) ;
}
[Theory]
[InlineData("https://url.com/My.Video.mkv")]
[InlineData("non-existent/path")]
public void GetExternalFiles_BadPaths_ReturnsNoSubtitles ( string path )
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock < IMediaSourceManager > ( ) ;
mediaSourceManager . Setup ( m = > m . GetPathProtocol ( It . IsRegex ( @"http.*" ) ) )
. Returns ( MediaProtocol . Http ) ;
BaseItem . MediaSourceManager = mediaSourceManager . Object ;
var video = new Movie
{
Path = path
} ;
var files = _subtitleResolver . GetExternalFiles ( video , Mock . Of < IDirectoryService > ( ) , false ) ;
Assert . Empty ( files ) ;
}
[Theory]
[InlineData("My.Video.srt", null)] // exact
[InlineData("My.Video.en.srt", "eng")]
[InlineData("MyVideo.en.srt", "eng")] // shorter title
[InlineData("My _ Video.en.srt", "eng")] // longer title
[InlineData("My.Video.en.srt", "eng", true)]
public void GetExternalFiles_FuzzyMatching_MatchesAndParsesToken ( string file , string? language , bool metadataDirectory = false )
{
BaseItem . MediaSourceManager = Mock . Of < IMediaSourceManager > ( ) ;
var video = new Movie
{
Path = VideoDirectoryPath + "/My.Video.mkv"
} ;
var directoryService = GetDirectoryServiceForExternalFile ( file , metadataDirectory ) ;
var streams = _subtitleResolver . GetExternalFiles ( video , directoryService , false ) . ToList ( ) ;
Assert . Single ( streams ) ;
var actual = streams [ 0 ] ;
Assert . Equal ( language , actual . Language ) ;
Assert . Null ( actual . Title ) ;
}
[Theory]
[InlineData("My.Video.mp3")]
[InlineData("My.Video.png")]
[InlineData("My.Video.txt")]
[InlineData("My.Video Sequel.srt")]
[InlineData("Some.Other.Video.srt")]
public void GetExternalFiles_FuzzyMatching_RejectsNonMatches ( string file )
{
BaseItem . MediaSourceManager = Mock . Of < IMediaSourceManager > ( ) ;
var video = new Movie
{
Path = VideoDirectoryPath + "/My.Video.mkv"
} ;
var directoryService = GetDirectoryServiceForExternalFile ( file ) ;
var streams = _subtitleResolver . GetExternalFiles ( video , directoryService , false ) . ToList ( ) ;
Assert . Empty ( streams ) ;
}
[Theory]
[InlineData("https://url.com/My.Video.mkv")]
[InlineData("non-existent/path")]
[InlineData(VideoDirectoryPath)] // valid but no files found for this test
public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles ( string path )
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock < IMediaSourceManager > ( ) ;
mediaSourceManager . Setup ( m = > m . GetPathProtocol ( It . IsRegex ( @"http.*" ) ) )
. Returns ( MediaProtocol . Http ) ;
BaseItem . MediaSourceManager = mediaSourceManager . Object ;
var video = new Movie
{
Path = path
} ;
var directoryService = new Mock < IDirectoryService > ( MockBehavior . Strict ) ;
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsAny < string > ( ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( Array . Empty < string > ( ) ) ;
var mediaEncoder = Mock . Of < IMediaEncoder > ( MockBehavior . Strict ) ;
var subtitleResolver = new SubtitleResolver ( _localizationManager , mediaEncoder , new NamingOptions ( ) ) ;
var streams = await subtitleResolver . GetExternalStreamsAsync ( video , 0 , directoryService . Object , false , CancellationToken . None ) ;
Assert . Empty ( streams ) ;
}
private static TheoryData < string , MediaStream [ ] , MediaStream [ ] > GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data ( )
{
var data = new TheoryData < string , MediaStream [ ] , MediaStream [ ] > ( ) ;
// filename and stream have no metadata set
string file = "My.Video.srt" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , null , null , 0 )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , null , null , 0 )
} ) ;
// filename has metadata
file = "My.Video.Title1.default.forced.en.srt" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , null , null , 0 )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title1" , 0 , true , true )
} ) ;
// single stream with metadata
file = "My.Video.mks" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title" , 0 , true , true )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title" , 0 , true , true )
} ) ;
// stream wins for title/language, filename wins for flags when conflicting
file = "My.Video.Title2.default.forced.en.srt" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "fra" , "Metadata" , 0 )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "fra" , "Metadata" , 0 , true , true )
} ) ;
// multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
file = "My.Video.Title3.default.forced.en.srt" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , null , null , 0 , true , true ) ,
CreateMediaStream ( VideoDirectoryPath + "/" + file , "fra" , "Metadata" , 1 )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title3" , 0 , true , true ) ,
CreateMediaStream ( VideoDirectoryPath + "/" + file , "fra" , "Metadata" , 1 )
} ) ;
return data ;
}
[Theory]
[MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))]
public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly ( string file , MediaStream [ ] inputStreams , MediaStream [ ] expectedStreams )
{
BaseItem . MediaSourceManager = Mock . Of < IMediaSourceManager > ( ) ;
var video = new Movie
{
Path = VideoDirectoryPath + "/My.Video.mkv"
} ;
var mediaEncoder = new Mock < IMediaEncoder > ( MockBehavior . Strict ) ;
mediaEncoder . Setup ( me = > me . GetMediaInfo ( It . IsAny < MediaInfoRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
. Returns < MediaInfoRequest , CancellationToken > ( ( _ , _ ) = > Task . FromResult ( new MediaBrowser . Model . MediaInfo . MediaInfo
{
MediaStreams = inputStreams . ToList ( )
} ) ) ;
var subtitleResolver = new SubtitleResolver ( _localizationManager , mediaEncoder . Object , new NamingOptions ( ) ) ;
var directoryService = GetDirectoryServiceForExternalFile ( file ) ;
var streams = await subtitleResolver . GetExternalStreamsAsync ( video , 0 , directoryService , false , CancellationToken . None ) ;
Assert . Equal ( expectedStreams . Length , streams . Count ) ;
for ( var i = 0 ; i < expectedStreams . Length ; i + + )
{
var expected = expectedStreams [ i ] ;
var actual = streams [ i ] ;
Assert . True ( actual . IsExternal ) ;
Assert . Equal ( expected . Index , actual . Index ) ;
Assert . Equal ( expected . Type , actual . Type ) ;
Assert . Equal ( expected . Path , actual . Path ) ;
Assert . Equal ( expected . IsDefault , actual . IsDefault ) ;
Assert . Equal ( expected . IsForced , actual . IsForced ) ;
Assert . Equal ( expected . Language , actual . Language ) ;
Assert . Equal ( expected . Title , actual . Title ) ;
}
}
[Theory]
[InlineData(1, 1)]
[InlineData(1, 2)]
[InlineData(2, 1)]
[InlineData(2, 2)]
public async void GetExternalStreams_StreamIndex_HandlesFilesAndContainers ( int fileCount , int streamCount )
{
BaseItem . MediaSourceManager = Mock . Of < IMediaSourceManager > ( ) ;
var video = new Movie
{
Path = VideoDirectoryPath + "/My.Video.mkv"
} ;
var files = new string [ fileCount ] ;
for ( int i = 0 ; i < fileCount ; i + + )
{
files [ i ] = $"{VideoDirectoryPath}/MyVideo.{i}.srt" ;
}
var directoryService = new Mock < IDirectoryService > ( MockBehavior . Strict ) ;
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsRegex ( VideoDirectoryRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( files ) ;
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsRegex ( MetadataDirectoryRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( Array . Empty < string > ( ) ) ;
List < MediaStream > GenerateMediaStreams ( )
{
var mediaStreams = new List < MediaStream > ( ) ;
for ( int i = 0 ; i < streamCount ; i + + )
{
mediaStreams . Add ( new ( ) ) ;
}
return mediaStreams ;
}
var mediaEncoder = new Mock < IMediaEncoder > ( MockBehavior . Strict ) ;
mediaEncoder . Setup ( me = > me . GetMediaInfo ( It . IsAny < MediaInfoRequest > ( ) , It . IsAny < CancellationToken > ( ) ) )
. Returns < MediaInfoRequest , CancellationToken > ( ( _ , _ ) = > Task . FromResult ( new MediaBrowser . Model . MediaInfo . MediaInfo
{
MediaStreams = GenerateMediaStreams ( )
} ) ) ;
var subtitleResolver = new SubtitleResolver ( _localizationManager , mediaEncoder . Object , new NamingOptions ( ) ) ;
int startIndex = 1 ;
var streams = await subtitleResolver . GetExternalStreamsAsync ( video , startIndex , directoryService . Object , false , CancellationToken . None ) ;
Assert . Equal ( fileCount * streamCount , streams . Count ) ;
for ( var i = 0 ; i < streams . Count ; i + + )
{
Assert . Equal ( startIndex + i , streams [ i ] . Index ) ;
// intentional integer division to ensure correct number of streams come back from each file
Assert . Matches ( @ $".*\.{i / streamCount}\.srt" , streams [ i ] . Path ) ;
}
}
private static MediaStream CreateMediaStream ( string path , string? language , string? title , int index , bool isForced = false , bool isDefault = false )
{
return new MediaStream
{
Index = index ,
Type = MediaStreamType . Subtitle ,
Path = path ,
IsDefault = isDefault ,
IsForced = isForced ,
Language = language ,
Title = title
} ;
}
/// <summary>
/// Provides an <see cref="IDirectoryService"/> that when queried for the test video/metadata directory will return a path including the provided file name.
/// </summary>
/// <param name="file">The name of the file to locate.</param>
/// <param name="useMetadataDirectory"><c>true</c> if the file belongs in the metadata directory.</param>
/// <returns>A mocked <see cref="IDirectoryService"/>.</returns>
public static IDirectoryService GetDirectoryServiceForExternalFile ( string file , bool useMetadataDirectory = false )
{
var directoryService = new Mock < IDirectoryService > ( MockBehavior . Strict ) ;
if ( useMetadataDirectory )
{
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsRegex ( VideoDirectoryRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( Array . Empty < string > ( ) ) ;
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsRegex ( MetadataDirectoryRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( new [ ] { MetadataDirectoryPath + "/" + file } ) ;
}
else
{
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsRegex ( VideoDirectoryRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( new [ ] { VideoDirectoryPath + "/" + file } ) ;
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsRegex ( MetadataDirectoryRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( Array . Empty < string > ( ) ) ;
}
return directoryService . Object ;
}
}