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.IO ;
using MediaBrowser.Model.MediaInfo ;
using MediaBrowser.Providers.MediaInfo ;
using Microsoft.Extensions.Logging ;
using Moq ;
using Xunit ;
namespace Jellyfin.Providers.Tests.MediaInfo ;
public class MediaInfoResolverTests
{
public const string VideoDirectoryPath = "Test Data/Video" ;
public const string VideoDirectoryRegex = @"Test Data[/\\]Video" ;
public const string MetadataDirectoryPath = "library/00/00000000000000000000000000000000" ;
public 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 ( )
}
} ) ) ;
var fileSystem = new Mock < IFileSystem > ( MockBehavior . Strict ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsAny < string > ( ) ) )
. Returns ( false ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsRegex ( VideoDirectoryRegex ) ) )
. Returns ( true ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsRegex ( MetadataDirectoryRegex ) ) )
. Returns ( true ) ;
_subtitleResolver = new SubtitleResolver ( Mock . Of < ILogger < SubtitleResolver > > ( ) , _localizationManager , mediaEncoder . Object , fileSystem . Object , new NamingOptions ( ) ) ;
}
[Fact]
public void GetExternalFiles_BadProtocol_ReturnsNoSubtitles ( )
{
// 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 = "https://url.com/My.Video.mkv"
} ;
Assert . Empty ( _subtitleResolver . GetExternalFiles ( video , Mock . Of < IDirectoryService > ( ) , false ) ) ;
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetExternalFiles_MissingDirectory_DirectoryNotQueried ( bool metadataDirectory )
{
BaseItem . MediaSourceManager = Mock . Of < IMediaSourceManager > ( ) ;
string containingFolderPath , metadataPath ;
if ( metadataDirectory )
{
containingFolderPath = VideoDirectoryPath ;
metadataPath = "invalid" ;
}
else
{
containingFolderPath = "invalid" ;
metadataPath = MetadataDirectoryPath ;
}
var video = new Mock < Movie > ( ) ;
video . Setup ( m = > m . Path )
. Returns ( VideoDirectoryPath + "/My.Video.mkv" ) ;
video . Setup ( m = > m . ContainingFolderPath )
. Returns ( containingFolderPath ) ;
video . Setup ( m = > m . GetInternalMetadataPath ( ) )
. Returns ( metadataPath ) ;
string pathNotFoundRegex = metadataDirectory ? MetadataDirectoryRegex : VideoDirectoryRegex ;
var directoryService = new Mock < IDirectoryService > ( MockBehavior . Strict ) ;
// any path other than test target exists and provides an empty listing
directoryService . Setup ( ds = > ds . GetFilePaths ( It . IsAny < string > ( ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
. Returns ( Array . Empty < string > ( ) ) ;
_subtitleResolver . GetExternalFiles ( video . Object , directoryService . Object , false ) ;
directoryService . Verify (
ds = > ds . GetFilePaths ( It . IsRegex ( pathNotFoundRegex ) , It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) ,
Times . Never ) ;
}
[Theory]
[InlineData("My.Video.mkv", "My.Video.srt", null)]
[InlineData("My.Video.mkv", "My.Video.en.srt", "eng")]
[InlineData("My.Video.mkv", "My.Video.en.srt", "eng", true)]
[InlineData("Example Movie (2021).mp4", "Example Movie (2021).English.Srt", "eng")]
[InlineData("[LTDB] Who Framed Roger Rabbit ( 1998 ) - [ Bluray - 1080 p ] . mkv ", " [ LTDB ] Who Framed Roger Rabbit ( 1998 ) - [ Bluray - 1080 p ] . en . srt ", " eng ")]
public void GetExternalFiles_NameMatching_MatchesAndParsesToken ( string movie , string file , string? language , bool metadataDirectory = false )
{
BaseItem . MediaSourceManager = Mock . Of < IMediaSourceManager > ( ) ;
var video = new Movie
{
Path = VideoDirectoryPath + "/" + movie
} ;
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("cover.jpg")]
[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_NameMatching_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(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 fileSystem = Mock . Of < IFileSystem > ( ) ;
var subtitleResolver = new SubtitleResolver ( Mock . Of < ILogger < SubtitleResolver > > ( ) , _localizationManager , mediaEncoder , fileSystem , new NamingOptions ( ) ) ;
var streams = await subtitleResolver . GetExternalStreamsAsync ( video , 0 , directoryService . Object , false , CancellationToken . None ) ;
Assert . Empty ( streams ) ;
}
public 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.sdh.en.srt" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , null , null , 0 )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title1" , 0 , true , true , true )
} ) ;
// single stream with metadata
file = "My.Video.mks" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title" , 0 , true , true , true )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "eng" , "Title" , 0 , true , true , true )
} ) ;
// stream wins for title/language, filename wins for flags when conflicting
file = "My.Video.Title2.default.forced.sdh.en.srt" ;
data . Add (
file ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "fra" , "Metadata" , 0 )
} ,
new [ ]
{
CreateMediaStream ( VideoDirectoryPath + "/" + file , "fra" , "Metadata" , 0 , true , 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 fileSystem = new Mock < IFileSystem > ( MockBehavior . Strict ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsRegex ( VideoDirectoryRegex ) ) )
. Returns ( true ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsRegex ( MetadataDirectoryRegex ) ) )
. Returns ( true ) ;
var subtitleResolver = new SubtitleResolver ( Mock . Of < ILogger < SubtitleResolver > > ( ) , _localizationManager , mediaEncoder . Object , fileSystem . 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 . IsHearingImpaired , actual . IsHearingImpaired ) ;
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}/My.Video.{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 ( )
{
Type = MediaStreamType . Subtitle
} ) ;
}
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 fileSystem = new Mock < IFileSystem > ( MockBehavior . Strict ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsRegex ( VideoDirectoryRegex ) ) )
. Returns ( true ) ;
fileSystem . Setup ( fs = > fs . DirectoryExists ( It . IsRegex ( MetadataDirectoryRegex ) ) )
. Returns ( true ) ;
var subtitleResolver = new SubtitleResolver ( Mock . Of < ILogger < SubtitleResolver > > ( ) , _localizationManager , mediaEncoder . Object , fileSystem . 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 , bool isHearingImpaired = false )
{
return new MediaStream
{
Index = index ,
Type = MediaStreamType . Subtitle ,
Path = path ,
IsDefault = isDefault ,
IsForced = isForced ,
IsHearingImpaired = isHearingImpaired ,
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 ;
}
}