aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-package.yml18
-rw-r--r--.github/workflows/codeql-analysis.yml2
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/Didl/DidlBuilder.cs1
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs2
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs6
-rw-r--r--Emby.Notifications/CoreNotificationTypes.cs8
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs20
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj2
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs37
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bg-BG.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json19
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json30
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/fil.json103
-rw-r--r--Emby.Server.Implementations/Localization/Core/kk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json2
-rw-r--r--Emby.Server.Implementations/Localization/iso6392.txt2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs7
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs9
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs2
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs23
-rw-r--r--Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs12
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs29
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs10
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs1
-rw-r--r--Jellyfin.Api/Controllers/TvShowsController.cs7
-rw-r--r--Jellyfin.Api/Jellyfin.Api.csproj2
-rw-r--r--Jellyfin.Data/Jellyfin.Data.csproj4
-rw-r--r--Jellyfin.Networking/Configuration/NetworkConfiguration.cs2
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs4
-rw-r--r--Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj4
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs75
-rw-r--r--Jellyfin.Server/Filters/ParameterObsoleteFilter.cs37
-rw-r--r--Jellyfin.Server/Jellyfin.Server.csproj4
-rw-r--r--Jellyfin.Server/Properties/AssemblyInfo.cs3
-rw-r--r--Jellyfin.Server/Startup.cs2
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs18
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/AssParser.cs14
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs10
-rw-r--r--MediaBrowser.Model/MediaBrowser.Model.csproj2
-rw-r--r--MediaBrowser.Model/Notifications/NotificationType.cs1
-rw-r--r--MediaBrowser.Model/Querying/NextUpQuery.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs14
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs184
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs12
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs5
-rw-r--r--MediaBrowser.sln9
-rw-r--r--deployment/Dockerfile.debian.amd642
-rw-r--r--deployment/Dockerfile.debian.arm642
-rw-r--r--deployment/Dockerfile.debian.armhf2
-rw-r--r--deployment/Dockerfile.linux.amd642
-rw-r--r--deployment/Dockerfile.linux.amd64-musl2
-rw-r--r--deployment/Dockerfile.linux.arm642
-rw-r--r--deployment/Dockerfile.linux.armhf2
-rw-r--r--deployment/Dockerfile.macos2
-rw-r--r--deployment/Dockerfile.portable2
-rw-r--r--deployment/Dockerfile.ubuntu.amd642
-rw-r--r--deployment/Dockerfile.ubuntu.arm642
-rw-r--r--deployment/Dockerfile.ubuntu.armhf2
-rw-r--r--deployment/Dockerfile.windows.amd642
-rw-r--r--fedora/jellyfin.spec2
-rw-r--r--jellyfin.ruleset2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj6
-rw-r--r--tests/Jellyfin.Api.Tests/ParseNetworkTests.cs88
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs16
-rw-r--r--tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs16
-rw-r--r--tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj2
-rw-r--r--tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs96
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs38
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs35
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ass22
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/example.srt8
-rw-r--r--tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj4
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs12
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs1
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj40
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs103
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs107
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs72
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs70
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs83
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs91
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo185
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo230
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/Season 01.nfo86
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Best of 1980-1990.nfo29
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo111
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Test Data/U2.nfo70
102 files changed, 2196 insertions, 278 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 0a63b329b..20f4dfe33 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -193,6 +193,10 @@ jobs:
pool:
vmImage: 'ubuntu-latest'
+ variables:
+ - name: JellyfinVersion
+ value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
+
steps:
- task: UseDotNet@2
displayName: 'Use .NET 5.0 sdk'
@@ -204,9 +208,15 @@ jobs:
displayName: 'Build Stable Nuget packages'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
- command: 'pack'
- packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
- versioningScheme: 'off'
+ command: 'custom'
+ projects: |
+ Jellyfin.Data/Jellyfin.Data.csproj
+ MediaBrowser.Common/MediaBrowser.Common.csproj
+ MediaBrowser.Controller/MediaBrowser.Controller.csproj
+ MediaBrowser.Model/MediaBrowser.Model.csproj
+ Emby.Naming/Emby.Naming.csproj
+ custom: 'pack'
+ arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
- task: DotNetCoreCLI@2
displayName: 'Build Unstable Nuget packages'
@@ -233,7 +243,7 @@ jobs:
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'NugetOrg'
allowPackageConflicts: true # This ignores an error if the version already exists
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 538894818..3e456f909 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -24,7 +24,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: '5.0.100'
+ dotnet-version: '5.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 33799f24b..1200275d5 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -80,6 +80,7 @@
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei)
+ - [obradovichv](https://github.com/obradovichv)
- [oddstr13](https://github.com/oddstr13)
- [orryverducci](https://github.com/orryverducci)
- [petermcneil](https://github.com/petermcneil)
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index abaf522bc..8b50d47fb 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -96,6 +96,7 @@ namespace Emby.Dlna.Didl
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
{
+ // If this using are changed to single lines, then write.Flush needs to be appended before the return.
using (var writer = XmlWriter.Create(builder, settings))
{
// writer.WriteStartDocument();
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 3f7b558f6..82490ec31 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -315,7 +315,7 @@ namespace Emby.Dlna.Main
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
// DLNA will only work over http, so we must reset to http:// : {port}
uri.Scheme = "http://";
- uri.Port = _netConfig.PublicPort;
+ uri.Port = _netConfig.HttpServerPortNumber;
var device = new SsdpRootDevice
{
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 311fae240..315be1e8b 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -896,16 +896,16 @@ namespace Emby.Dlna.PlayTo
var parts = url.Split('/');
- for (var i = 0; i < parts.Length; i++)
+ for (var i = 0; i < parts.Length - 1; i++)
{
var part = parts[i];
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
- if (parts.Length > i + 1)
+ if (Guid.TryParse(parts[i + 1], out var result))
{
- return Guid.Parse(parts[i + 1]);
+ return result;
}
}
}
diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs
index a602b7221..ec3490e23 100644
--- a/Emby.Notifications/CoreNotificationTypes.cs
+++ b/Emby.Notifications/CoreNotificationTypes.cs
@@ -76,10 +76,6 @@ namespace Emby.Notifications
},
new NotificationTypeInfo
{
- Type = NotificationType.CameraImageUploaded.ToString()
- },
- new NotificationTypeInfo
- {
Type = NotificationType.UserLockedOut.ToString()
},
new NotificationTypeInfo
@@ -114,10 +110,6 @@ namespace Emby.Notifications
{
note.Category = _localization.GetLocalizedString("Plugin");
}
- else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
- {
- note.Category = _localization.GetLocalizedString("Sync");
- }
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("User");
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index d5e1f5124..8a901516c 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -582,16 +582,22 @@ namespace Emby.Server.Implementations.Dto
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
- // Only add BlurHash for the person's image.
- baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
- foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
+ if (dto.ImageBlurHashes != null)
{
- baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
- foreach (var (imageId, blurHashValue) in blurHash)
+ // Only add BlurHash for the person's image.
+ baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+ foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
- if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
+ if (blurHash != null)
{
- baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
+ baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
+ foreach (var (imageId, blurHashValue) in blurHash)
+ {
+ if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
+ {
+ baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
+ }
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 67f23f055..08047ba47 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
- <PackageReference Include="sharpcompress" Version="0.26.0" />
+ <PackageReference Include="sharpcompress" Version="0.27.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 3cb025111..5ebc9b61b 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -582,9 +582,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
-
- return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
+ return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
@@ -594,16 +592,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
{
- return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
+ return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
}
- var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
+ var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
if (extensions != null && extensions.Count > 0)
{
@@ -625,10 +623,10 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
var directoryInfo = new DirectoryInfo(path);
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ var enumerationOptions = GetEnumerationOptions(recursive);
- return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
- .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
+ return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
+ .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -638,8 +636,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
- return Directory.EnumerateDirectories(path, "*", searchOption);
+ return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
@@ -649,16 +646,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
{
- return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption);
+ return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
}
- var files = Directory.EnumerateFiles(path, "*", searchOption);
+ var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
if (extensions != null && extensions.Length > 0)
{
@@ -679,8 +676,16 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
- var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
- return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
+ return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
+ }
+
+ private EnumerationOptions GetEnumerationOptions(bool recursive)
+ {
+ return new EnumerationOptions
+ {
+ RecurseSubdirectories = recursive,
+ IgnoreInaccessible = true
+ };
}
private static void RunProcess(string path, string args, string workingDirectory)
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 41561916f..c76d41e5c 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
// It's a directory-based playlist if the directory contains a playlist file
- var filePaths = Directory.EnumerateFiles(args.Path);
+ var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
{
return new Playlist
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 1fed83276..7ff30df71 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
"Channels": "Канали",
"ChapterNameValue": "Глава {0}",
- "Collections": "Колекции",
+ "Collections": "Поредици",
"DeviceOfflineWithName": "{0} се разкачи",
"DeviceOnlineWithName": "{0} е свързан",
"FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
@@ -113,5 +113,8 @@
"TasksChannelsCategory": "Интернет Канали",
"TasksApplicationCategory": "Приложение",
"TasksLibraryCategory": "Библиотека",
- "TasksMaintenanceCategory": "Поддръжка"
+ "TasksMaintenanceCategory": "Поддръжка",
+ "Undefined": "Неопределено",
+ "Forced": "Принудително",
+ "Default": "По подразбиране"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index b7852eccb..fd8437b6d 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -18,10 +18,10 @@
"HeaderAlbumArtists": "Artistes del Àlbum",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
- "HeaderFavoriteArtists": "Artistes Preferits",
- "HeaderFavoriteEpisodes": "Episodis Preferits",
- "HeaderFavoriteShows": "Programes Preferits",
- "HeaderFavoriteSongs": "Cançons Preferides",
+ "HeaderFavoriteArtists": "Artistes Predilectes",
+ "HeaderFavoriteEpisodes": "Episodis Predilectes",
+ "HeaderFavoriteShows": "Programes Predilectes",
+ "HeaderFavoriteSongs": "Cançons Predilectes",
"HeaderLiveTV": "TV en Directe",
"HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups d'Enregistrament",
@@ -36,7 +36,7 @@
"MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
"MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
- "MixedContent": "Contingut mesclat",
+ "MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules",
"Music": "Música",
"MusicVideos": "Vídeos musicals",
@@ -76,7 +76,7 @@
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
"Sync": "Sincronitzar",
- "System": "System",
+ "System": "Sistema",
"TvShows": "Espectacles de TV",
"User": "User",
"UserCreatedWithName": "S'ha creat l'usuari {0}",
@@ -113,5 +113,10 @@
"TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicació",
"TasksLibraryCategory": "Biblioteca",
- "TasksMaintenanceCategory": "Manteniment"
+ "TasksMaintenanceCategory": "Manteniment",
+ "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
+ "TaskCleanActivityLog": "Buidar Registre d'Activitat",
+ "Undefined": "Indefinit",
+ "Forced": "Forçat",
+ "Default": "Defecto"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 6ab22b8a4..9d82b5878 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
- "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
+ "AuthenticationSucceededWithUserName": "{0} wurde angemeldet",
"Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle",
@@ -94,22 +94,22 @@
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Meta Einstellungen.",
"TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
- "TaskRefreshChannelsDescription": "Erneuere Internet Kanal Informationen.",
- "TaskRefreshChannels": "Erneuere Kanäle",
- "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
- "TaskCleanTranscode": "Lösche Transkodier Pfad",
- "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
- "TaskUpdatePlugins": "Update Plugins",
- "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
- "TaskRefreshPeople": "Erneuere Schauspieler",
- "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
- "TaskCleanLogs": "Lösche Log Pfad",
- "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
+ "TaskRefreshChannelsDescription": "Aktualisiere Internet Kanal Informationen.",
+ "TaskRefreshChannels": "Aktualisiere Kanäle",
+ "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, welche älter als einen Tag sind.",
+ "TaskCleanTranscode": "Lösche Transkodier-Pfad",
+ "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
+ "TaskUpdatePlugins": "Aktualisiere Plugins",
+ "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
+ "TaskRefreshPeople": "Aktualisiere Schauspieler",
+ "TaskCleanLogsDescription": "Lösche Log Dateien, die älter als {0} Tage sind.",
+ "TaskCleanLogs": "Lösche Log-Verzeichnis",
+ "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.",
"TaskRefreshLibrary": "Scanne Medien-Bibliothek",
- "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
+ "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, welche Kapitel besitzen.",
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
- "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
- "TaskCleanCache": "Leere Cache Pfad",
+ "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.",
+ "TaskCleanCache": "Leere Zwischenspeicher",
"TasksChannelsCategory": "Internet Kanäle",
"TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index 03c6d5f5d..6d2a5c7ac 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -116,5 +116,6 @@
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanActivityLog": "Limpiar Registro de Actividades",
"Undefined": "Sin definir",
- "Forced": "Forzado"
+ "Forced": "Forzado",
+ "Default": "Por Defecto"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 7eb8e36e7..e9e4f61b8 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "پخش صدا آغاز شد",
"NotificationOptionAudioPlaybackStopped": "پخش صدا متوقف شد",
"NotificationOptionCameraImageUploaded": "تصاویر دوربین آپلود شد",
- "NotificationOptionInstallationFailed": "نصب شکست خورد",
+ "NotificationOptionInstallationFailed": "نصب ناموفق",
"NotificationOptionNewLibraryContent": "محتوای جدید افزوده شد",
"NotificationOptionPluginError": "خرابی افزونه",
"NotificationOptionPluginInstalled": "افزونه نصب شد",
@@ -115,5 +115,8 @@
"TasksLibraryCategory": "کتابخانه",
"TasksMaintenanceCategory": "تعمیر",
"Forced": "اجباری",
- "Default": "پیشفرض"
+ "Default": "پیشفرض",
+ "TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.",
+ "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت",
+ "Undefined": "تعریف نشده"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index e5ca676a4..f18a1c030 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -3,101 +3,101 @@
"ValueSpecialEpisodeName": "Espesyal - {0}",
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
- "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
- "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
+ "UserStartedPlayingItemWithValues": "Si {0} ay nagpla-play ng {1} sa {2}",
+ "UserPolicyUpdatedWithName": "Ang user policy ay nai-update para kay {0}",
"UserPasswordChangedWithName": "Napalitan na ang password ni {0}",
- "UserOnlineFromDevice": "Si {0} ay nakakonekta galing sa {1}",
- "UserOfflineFromDevice": "Si {0} ay nadiskonekta galing sa {1}",
+ "UserOnlineFromDevice": "Si {0} ay naka-konekta galing sa {1}",
+ "UserOfflineFromDevice": "Si {0} ay na-diskonekta galing sa {1}",
"UserLockedOutWithName": "Si {0} ay nalock out",
"UserDownloadingItemWithValues": "Nagdadownload si {0} ng {1}",
"UserDeletedWithName": "Natanggal na is user {0}",
"UserCreatedWithName": "Nagawa na si user {0}",
"User": "User",
- "TvShows": "Pelikula",
+ "TvShows": "Mga Palabas sa Telebisyon",
"System": "Sistema",
"Sync": "Pag-sync",
- "SubtitleDownloadFailureFromForItem": "Hindi naidownload ang subtitles {0} para sa {1}",
- "StartupEmbyServerIsLoading": "Nagloload ang Jellyfin Server. Sandaling maghintay.",
- "Songs": "Kanta",
- "Shows": "Pelikula",
+ "SubtitleDownloadFailureFromForItem": "Hindi nai-download ang subtitles {0} para sa {1}",
+ "StartupEmbyServerIsLoading": "Naglo-load ang Jellyfin Server. Mangyaring subukan ulit sandali.",
+ "Songs": "Mga Kanta",
+ "Shows": "Mga Pelikula",
"ServerNameNeedsToBeRestarted": "Kailangan irestart ang {0}",
"ScheduledTaskStartedWithName": "Nagsimula na ang {0}",
- "ScheduledTaskFailedWithName": "Hindi gumana and {0}",
- "ProviderValue": "Ang provider ay {0}",
+ "ScheduledTaskFailedWithName": "Hindi gumana ang {0}",
+ "ProviderValue": "Tagapagtustos: {0}",
"PluginUpdatedWithName": "Naiupdate na ang {0}",
"PluginUninstalledWithName": "Naiuninstall na ang {0}",
"PluginInstalledWithName": "Nainstall na ang {0}",
"Plugin": "Plugin",
- "Playlists": "Playlists",
- "Photos": "Larawan",
+ "Playlists": "Mga Playlist",
+ "Photos": "Mga Larawan",
"NotificationOptionVideoPlaybackStopped": "Huminto na ang pelikula",
"NotificationOptionVideoPlayback": "Nagsimula na ang pelikula",
- "NotificationOptionUserLockedOut": "Nakalock out ang user",
+ "NotificationOptionUserLockedOut": "Naka-lock out ang user",
"NotificationOptionTaskFailed": "Hindi gumana ang scheduled task",
- "NotificationOptionServerRestartRequired": "Kailangan irestart ang server",
- "NotificationOptionPluginUpdateInstalled": "Naiupdate na ang plugin",
- "NotificationOptionPluginUninstalled": "Naiuninstall na ang plugin",
+ "NotificationOptionServerRestartRequired": "Kailangan i-restart ang server",
+ "NotificationOptionPluginUpdateInstalled": "Nai-update na ang plugin",
+ "NotificationOptionPluginUninstalled": "Nai-uninstall na ang plugin",
"NotificationOptionPluginInstalled": "Nainstall na ang plugin",
"NotificationOptionPluginError": "Hindi gumagana ang plugin",
"NotificationOptionNewLibraryContent": "May bagong content na naidagdag",
"NotificationOptionInstallationFailed": "Hindi nainstall ng mabuti",
- "NotificationOptionCameraImageUploaded": "Naiupload na ang picture",
+ "NotificationOptionCameraImageUploaded": "Naiupload na ang litrato",
"NotificationOptionAudioPlaybackStopped": "Huminto na ang patugtog",
"NotificationOptionAudioPlayback": "Nagsimula na ang patugtog",
"NotificationOptionApplicationUpdateInstalled": "Naiupdate na ang aplikasyon",
"NotificationOptionApplicationUpdateAvailable": "May bagong update ang aplikasyon",
- "NewVersionIsAvailable": "May bagong version ng Jellyfin Server na pwede idownload.",
- "NameSeasonUnknown": "Hindi alam ang season",
+ "NewVersionIsAvailable": "May bagong version ng Jellyfin Server na pwede i-download.",
+ "NameSeasonUnknown": "Hindi matukoy ang season",
"NameSeasonNumber": "Season {0}",
"NameInstallFailed": "Hindi nainstall ang {0}",
- "MusicVideos": "Music video",
- "Music": "Kanta",
- "Movies": "Pelikula",
+ "MusicVideos": "Mga Music video",
+ "Music": "Mga Kanta",
+ "Movies": "Mga Pelikula",
"MixedContent": "Halo-halong content",
"MessageServerConfigurationUpdated": "Naiupdate na ang server configuration",
"MessageNamedServerConfigurationUpdatedWithValue": "Naiupdate na ang server configuration section {0}",
- "MessageApplicationUpdatedTo": "Ang Jellyfin Server ay naiupdate to {0}",
+ "MessageApplicationUpdatedTo": "Ang bersyon ng Jellyfin Server ay naiupdate sa {0}",
"MessageApplicationUpdated": "Naiupdate na ang Jellyfin Server",
"Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}",
- "LabelIpAddressValue": "Ang IP Address ay {0}",
+ "LabelIpAddressValue": "IP address: {0}",
"ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
"ItemAddedWithName": "Naidagdag ang {0} sa librerya",
"Inherit": "Manahin",
"HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod",
"HeaderLiveTV": "Live TV",
- "HeaderFavoriteSongs": "Paboritong Kanta",
- "HeaderFavoriteShows": "Paboritong Pelikula",
- "HeaderFavoriteEpisodes": "Paboritong Episodes",
- "HeaderFavoriteArtists": "Paboritong Artista",
- "HeaderFavoriteAlbums": "Paboritong Albums",
- "HeaderContinueWatching": "Ituloy Manood",
- "HeaderAlbumArtists": "Artista ng Album",
- "Genres": "Kategorya",
- "Folders": "Folders",
- "Favorites": "Paborito",
- "FailedLoginAttemptWithUserName": "maling login galing {0}",
- "DeviceOnlineWithName": "nakakonekta si {0}",
- "DeviceOfflineWithName": "nadiskonekta si {0}",
- "Collections": "Koleksyon",
+ "HeaderFavoriteSongs": "Mga Paboritong Kanta",
+ "HeaderFavoriteShows": "Mga Paboritong Pelikula",
+ "HeaderFavoriteEpisodes": "Mga Paboritong Episode",
+ "HeaderFavoriteArtists": "Mga Paboritong Artista",
+ "HeaderFavoriteAlbums": "Mga Paboritong Album",
+ "HeaderContinueWatching": "Magpatuloy sa Panonood",
+ "HeaderAlbumArtists": "Mga Artista ng Album",
+ "Genres": "Mga Kategorya",
+ "Folders": "Mga Folder",
+ "Favorites": "Mga Paborito",
+ "FailedLoginAttemptWithUserName": "Maling login galing kay/sa {0}",
+ "DeviceOnlineWithName": "Nakakonekta si/ang {0}",
+ "DeviceOfflineWithName": "Nadiskonekta si/ang {0}",
+ "Collections": "Mga Koleksyon",
"ChapterNameValue": "Kabanata {0}",
- "Channels": "Channel",
- "CameraImageUploadedFrom": "May bagong larawan na naupload galing {0}",
- "Books": "Libro",
- "AuthenticationSucceededWithUserName": "{0} na patunayan",
- "Artists": "Artista",
+ "Channels": "Mga Channel",
+ "CameraImageUploadedFrom": "May bagong larawan na naupload galing sa/kay {0}",
+ "Books": "Mga Libro",
+ "AuthenticationSucceededWithUserName": "Napatunayan si/ang {0}",
+ "Artists": "Mga Artista",
"Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
- "Albums": "Albums",
+ "Albums": "Mga Album",
"TaskRefreshLibrary": "Suriin and Librerya ng Medya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
- "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
+ "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng sistema.",
"TasksChannelsCategory": "Palabas sa internet",
"TasksLibraryCategory": "Librerya",
"TasksMaintenanceCategory": "Pagpapanatili",
- "HomeVideos": "Sariling pelikula",
+ "HomeVideos": "Sariling video/pelikula",
"TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
"TaskRefreshPeople": "I-refresh ang Tauhan",
"TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
@@ -105,14 +105,17 @@
"TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
"TaskRefreshChannels": "I-refresh ang Channels",
"TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
- "TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
+ "TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa awtomatikong pag-update.",
"TaskUpdatePlugins": "I-update ang Plugins",
"TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
"TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
"TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
"TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
"TaskCleanCache": "Linisin and Direktoryo ng Cache",
- "TasksApplicationCategory": "Application",
+ "TasksApplicationCategory": "Aplikasyon",
"TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
- "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
+ "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas luma sa nakatakda na edad.",
+ "Default": "Default",
+ "Undefined": "Hindi tiyak",
+ "Forced": "Sapilitan"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 7ce9822b6..befe4e14f 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -94,7 +94,7 @@
"VersionNumber": "Nusqasy {0}",
"Default": "Ádepki",
"TaskDownloadMissingSubtitles": "Joq sýbtıtrlerdi júktep alý",
- "TaskRefreshChannels": "Arnalardy jańartý",
+ "TaskRefreshChannels": "Arnalardy jańǵyrtý",
"TaskCleanTranscode": "Qaıta kodtaý katalogyn tazalaý",
"TaskUpdatePlugins": "Plagınderdi jańartý",
"TaskRefreshPeople": "Adamdardy jańartý",
@@ -110,7 +110,7 @@
"Undefined": "Anyqtalmady",
"Forced": "Májbúrli",
"TaskDownloadMissingSubtitlesDescription": "Metaderekter teńshelimi negіzіnde joq sýbtıtrlerdі Internetten іzdeıdі.",
- "TaskRefreshChannelsDescription": "Internet-arnalar málimetterin jańartady.",
+ "TaskRefreshChannelsDescription": "Internet-arnalar málimetterin jańǵyrtady.",
"TaskCleanTranscodeDescription": "Bіr kúnnen asqan qaıta kodtaý faıldaryn joıady.",
"TaskUpdatePluginsDescription": "Avtomatty túrde jańartýǵa teńshelgen plagınder úshin jańartýlardy júktep alady jáne ornatady.",
"TaskRefreshPeopleDescription": "Tasyǵyshhanadaǵy aktórler men rejısórler metaderekterіn jańartady.",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 3b016fe62..d5bca9f6c 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -117,5 +117,6 @@
"TaskCleanActivityLog": "Tøm aktivitetslogg",
"Undefined": "Udefinert",
"Forced": "Tvungen",
- "Default": "Standard"
+ "Default": "Standard",
+ "TaskCleanActivityLogDescription": "Sletter oppføringer i aktivitetsloggen som er eldre enn den konfigurerte alderen."
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 885663eed..c6b904045 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -12,7 +12,7 @@
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
- "Favorites": "Favorilerim",
+ "Favorites": "Favoriler",
"Folders": "Klasörler",
"Genres": "Türler",
"HeaderAlbumArtists": "Albüm Sanatçıları",
@@ -117,5 +117,6 @@
"TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
"TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
"Undefined": "Bilinmeyen",
- "Default": "Varsayılan"
+ "Default": "Varsayılan",
+ "Forced": "Zorla"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 435e294ef..3dad21dcb 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -113,5 +113,9 @@
"TaskCleanCache": "清理緩存目錄",
"TasksChannelsCategory": "互聯網頻道",
"TasksLibraryCategory": "庫",
- "TaskRefreshPeople": "刷新人物"
+ "TaskRefreshPeople": "刷新人物",
+ "TaskCleanActivityLog": "清理活動記錄",
+ "Undefined": "未定義",
+ "Forced": "強制",
+ "Default": "預設"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 6494c0b54..affb0e099 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -1,6 +1,6 @@
{
"Albums": "專輯",
- "AppDeviceValues": "軟體:{0},裝置:{1}",
+ "AppDeviceValues": "App:{0},裝置:{1}",
"Application": "應用程式",
"Artists": "演出者",
"AuthenticationSucceededWithUserName": "{0} 成功授權",
diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt
index 40f8614f1..488901822 100644
--- a/Emby.Server.Implementations/Localization/iso6392.txt
+++ b/Emby.Server.Implementations/Localization/iso6392.txt
@@ -77,6 +77,8 @@ chb|||Chibcha|chibcha
che||ce|Chechen|tchétchène
chg|||Chagatai|djaghataï
chi|zho|zh|Chinese|chinois
+chi|zho|zh-tw|Chinese; Traditional|chinois
+chi|zho|zh-hk|Chinese; Hong Kong|chinois
chk|||Chuukese|chuuk
chm|||Mari|mari
chn|||Chinook jargon|chinook, jargon
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index 29440b64a..d3cf3bf3f 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
lock (_lastExecutionResultSyncLock)
{
- using FileStream createStream = File.OpenWrite(path);
+ using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
JsonSerializer.SerializeAsync(createStream, value, _jsonOptions);
}
}
@@ -577,9 +577,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
var path = GetConfigurationFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- var json = JsonSerializer.Serialize(triggers, _jsonOptions);
- File.WriteAllText(path, json, Encoding.UTF8);
+ using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ JsonSerializer.SerializeAsync(createStream, triggers, _jsonOptions);
}
/// <summary>
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index 184d155d4..fedb5deb0 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -80,10 +80,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
// Delete log files more than n days old
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
- // Only delete the .txt log files, the *.log files created by serilog get managed by itself
- var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
- .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
- .ToList();
+ // Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
+ var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
+ .Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
+ && _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
var index = 0;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
index 8b67d37d7..3b40320ab 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs
@@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
var dueTime = triggerDate - now;
- logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:g}, which is {DueTime:g} from now.", taskName, triggerDate, dueTime);
+ logger.LogInformation("Daily trigger for {Task} set to fire at {TriggerDate:yyyy-MM-dd HH:mm:ss.fff zzz}, which is {DueTime:c} from now.", taskName, triggerDate, dueTime);
Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index f0734340b..839b62448 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -143,10 +143,31 @@ namespace Emby.Server.Implementations.TV
var allNextUp = seriesKeys
.Select(i => GetNextUp(i, currentUser, dtoOptions));
+ // If viewing all next up for all series, remove first episodes
+ // But if that returns empty, keep those first episodes (avoid completely empty view)
+ var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
+ var anyFound = false;
+
return allNextUp
.Where(i =>
{
- return i.Item1 != DateTime.MinValue;
+ if (request.DisableFirstEpisode)
+ {
+ return i.Item1 != DateTime.MinValue;
+ }
+
+ if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
+ {
+ anyFound = true;
+ return true;
+ }
+
+ if (!anyFound && i.Item1 == DateTime.MinValue)
+ {
+ return true;
+ }
+
+ return false;
})
.Select(i => i.Item2())
.Where(i => i != null);
diff --git a/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
new file mode 100644
index 000000000..56c9772b6
--- /dev/null
+++ b/Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Jellyfin.Api.Attributes
+{
+ /// <summary>
+ /// Attribute to mark a parameter as obsolete.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Parameter)]
+ public class ParameterObsoleteAttribute : Attribute
+ {
+ }
+}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index baa2e0636..e330f02b6 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -83,6 +83,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <remarks>
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+ /// Query parameters are obsolete.
/// </remarks>
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
@@ -106,20 +107,20 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId,
- [FromQuery] Guid? userId,
- [FromQuery] int? maxStreamingBitrate,
- [FromQuery] long? startTimeTicks,
- [FromQuery] int? audioStreamIndex,
- [FromQuery] int? subtitleStreamIndex,
- [FromQuery] int? maxAudioChannels,
- [FromQuery] string? mediaSourceId,
- [FromQuery] string? liveStreamId,
- [FromQuery] bool? autoOpenLiveStream,
- [FromQuery] bool? enableDirectPlay,
- [FromQuery] bool? enableDirectStream,
- [FromQuery] bool? enableTranscoding,
- [FromQuery] bool? allowVideoStreamCopy,
- [FromQuery] bool? allowAudioStreamCopy,
+ [FromQuery, ParameterObsolete] Guid? userId,
+ [FromQuery, ParameterObsolete] int? maxStreamingBitrate,
+ [FromQuery, ParameterObsolete] long? startTimeTicks,
+ [FromQuery, ParameterObsolete] int? audioStreamIndex,
+ [FromQuery, ParameterObsolete] int? subtitleStreamIndex,
+ [FromQuery, ParameterObsolete] int? maxAudioChannels,
+ [FromQuery, ParameterObsolete] string? mediaSourceId,
+ [FromQuery, ParameterObsolete] string? liveStreamId,
+ [FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
+ [FromQuery, ParameterObsolete] bool? enableDirectPlay,
+ [FromQuery, ParameterObsolete] bool? enableDirectStream,
+ [FromQuery, ParameterObsolete] bool? enableTranscoding,
+ [FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
+ [FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index fcdad4bc7..a55e4ad2f 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -57,6 +58,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <remarks>
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
+ /// Query parameters are obsolete.
/// </remarks>
/// <param name="name">The playlist name.</param>
/// <param name="ids">The item ids.</param>
@@ -70,10 +72,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
- [FromQuery] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
- [FromQuery] Guid? userId,
- [FromQuery] string? mediaType,
+ [FromQuery, ParameterObsolete] string? name,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
+ [FromQuery, ParameterObsolete] Guid? userId,
+ [FromQuery, ParameterObsolete] string? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{
if (ids.Count == 0)
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index dcb8e803b..16a47f2d8 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -371,6 +371,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
+ [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index ca18901e5..223f58859 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -67,6 +67,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
+ /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -81,7 +82,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
- [FromQuery] bool enableTotalRecordCount = true)
+ [FromQuery] bool enableTotalRecordCount = true,
+ [FromQuery] bool disableFirstEpisode = false)
{
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
@@ -95,7 +97,8 @@ namespace Jellyfin.Api.Controllers
SeriesId = seriesId,
StartIndex = startIndex,
UserId = userId ?? Guid.Empty,
- EnableTotalRecordCount = enableTotalRecordCount
+ EnableTotalRecordCount = enableTotalRecordCount,
+ DisableFirstEpisode = disableFirstEpisode
},
options);
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index f01f50cea..8437369b2 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.6.3" />
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 4fb5594d4..b95879de4 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -41,8 +41,8 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.1" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.2" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index 792e57f6a..91bf0015f 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -224,7 +224,7 @@ namespace Jellyfin.Networking.Configuration
public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
/// <summary>
- /// Gets or sets the known proxies.
+ /// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks.
/// </summary>
public string[] KnownProxies { get; set; } = Array.Empty<string>();
}
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 60b899519..8bb97937c 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -387,7 +387,7 @@ namespace Jellyfin.Networking.Manager
// Get the first LAN interface address that isn't a loopback.
var interfaces = CreateCollection(_interfaceAddresses
.Exclude(_bindExclusions)
- .Where(p => IsInLocalNetwork(p))
+ .Where(IsInLocalNetwork)
.OrderBy(p => p.Tag));
if (interfaces.Count > 0)
@@ -591,7 +591,7 @@ namespace Jellyfin.Networking.Manager
else // Used in testing only.
{
// Format is <IPAddress>,<Index>,<Name>: <next interface>. Set index to -ve to simulate a gateway.
- var interfaceList = MockNetworkSettings.Split(':');
+ var interfaceList = MockNetworkSettings.Split('|');
foreach (var details in interfaceList)
{
var parts = details.Split(',');
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 9e4a2065f..05052e5c0 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -26,11 +26,11 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index bbfc4fbd4..77fb64674 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Sockets;
using System.Reflection;
using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
@@ -20,6 +21,7 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
+using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
@@ -174,30 +176,33 @@ namespace Jellyfin.Server.Extensions
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
- /// <param name="knownProxies">A list of all known proxies to trust for X-Forwarded-For.</param>
+ /// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
/// <returns>The MVC builder.</returns>
- public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, IReadOnlyList<string> knownProxies)
+ public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, NetworkConfiguration config)
{
IMvcBuilder mvcBuilder = serviceCollection
.AddCors()
.AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
.Configure<ForwardedHeadersOptions>(options =>
{
+ // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
+ // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
+
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
- if (knownProxies.Count == 0)
+ if (config.KnownProxies.Length == 0)
{
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
}
else
{
- for (var i = 0; i < knownProxies.Count; i++)
- {
- if (IPHost.TryParse(knownProxies[i], out var host))
- {
- options.KnownProxies.Add(host.Address);
- }
- }
+ AddProxyAddresses(config, config.KnownProxies, options);
+ }
+
+ // Only set forward limit if we have some known proxies or some known networks.
+ if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
+ {
+ options.ForwardLimit = null;
}
})
.AddMvc(opts =>
@@ -308,10 +313,60 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<FileResponseFilter>();
+ c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<WebsocketModelFilter>();
});
}
+ /// <summary>
+ /// Sets up the proxy configuration based on the addresses in <paramref name="allowedProxies"/>.
+ /// </summary>
+ /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
+ /// <param name="allowedProxies">The string array to parse.</param>
+ /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
+ internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOptions options)
+ {
+ for (var i = 0; i < allowedProxies.Length; i++)
+ {
+ if (IPNetAddress.TryParse(allowedProxies[i], out var addr))
+ {
+ AddIpAddress(config, options, addr.Address, addr.PrefixLength);
+ }
+ else if (IPHost.TryParse(allowedProxies[i], out var host))
+ {
+ foreach (var address in host.GetAddresses())
+ {
+ AddIpAddress(config, options, addr.Address, addr.PrefixLength);
+ }
+ }
+ }
+ }
+
+ private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength)
+ {
+ if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6))
+ {
+ return;
+ }
+
+ // In order for dual-mode sockets to be used, IP6 has to be enabled in JF and an interface has to have an IP6 address.
+ if (addr.AddressFamily == AddressFamily.InterNetwork && config.EnableIPV6)
+ {
+ // If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format.
+ // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 .
+ addr = addr.MapToIPv6();
+ }
+
+ if (prefixLength == 32)
+ {
+ options.KnownProxies.Add(addr);
+ }
+ else
+ {
+ options.KnownNetworks.Add(new IPNetwork(addr, prefixLength));
+ }
+ }
+
private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
{
/*
diff --git a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs
new file mode 100644
index 000000000..e54044d0e
--- /dev/null
+++ b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using Jellyfin.Api.Attributes;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters
+{
+ /// <summary>
+ /// Mark parameter as deprecated if it has the <see cref="ParameterObsoleteAttribute"/>.
+ /// </summary>
+ public class ParameterObsoleteFilter : IOperationFilter
+ {
+ /// <inheritdoc />
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ foreach (var parameterDescription in context.ApiDescription.ParameterDescriptions)
+ {
+ if (parameterDescription
+ .CustomAttributes()
+ .OfType<ParameterObsoleteAttribute>()
+ .Any())
+ {
+ foreach (var parameter in operation.Parameters)
+ {
+ if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal))
+ {
+ parameter.Deprecated = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 5940cf938..3ebcc3279 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -40,8 +40,8 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.1" />
- <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.2" />
+ <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="prometheus-net" Version="4.1.1" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
diff --git a/Jellyfin.Server/Properties/AssemblyInfo.cs b/Jellyfin.Server/Properties/AssemblyInfo.cs
index 5de1e653d..7abf298b1 100644
--- a/Jellyfin.Server/Properties/AssemblyInfo.cs
+++ b/Jellyfin.Server/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -19,3 +20,5 @@ using System.Runtime.InteropServices;
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
+
+[assembly: InternalsVisibleTo("Jellyfin.Api.Tests")]
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 3395d2413..e56e61092 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -52,7 +52,7 @@ namespace Jellyfin.Server
{
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
- services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies);
+ services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinApiSwagger();
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 0a4967223..2f5b1d4a3 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -6,9 +6,7 @@ using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
@@ -22,6 +20,12 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
public interface IProviderManager
{
+ event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
+
+ event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
+
+ event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
+
/// <summary>
/// Queues the refresh.
/// </summary>
@@ -132,12 +136,14 @@ namespace MediaBrowser.Controller.Providers
/// </summary>
/// <param name="item">The item.</param>
/// <param name="updateType">Type of the update.</param>
- /// <returns>Task.</returns>
void SaveMetadata(BaseItem item, ItemUpdateType updateType);
/// <summary>
/// Saves the metadata.
/// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="updateType">Type of the update.</param>
+ /// <param name="savers">The metadata savers.</param>
void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers);
/// <summary>
@@ -179,12 +185,6 @@ namespace MediaBrowser.Controller.Providers
void OnRefreshComplete(BaseItem item);
double? GetRefreshProgress(Guid id);
-
- event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
-
- event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
-
- event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
}
public enum RefreshPriority
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index 5d3ab30d3..b0afb834b 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -344,8 +344,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
- var hasAspectRatio = item as IHasAspectRatio;
- if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
+ if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
{
hasAspectRatio.AspectRatio = val;
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
index 86b87fddd..e0b7914fb 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
using (var reader = new StreamReader(stream))
{
string line;
- while (reader.ReadLine() != "[Events]")
+ while (!string.Equals(reader.ReadLine(), "[Events]", StringComparison.Ordinal))
{
}
@@ -46,12 +46,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subEvent = new SubtitleTrackEvent { Id = eventIndex.ToString(_usCulture) };
eventIndex++;
- var sections = line.Substring(10).Split(',');
+ const string Dialogue = "Dialogue: ";
+ var sections = line.Substring(Dialogue.Length).Split(',');
subEvent.StartPositionTicks = GetTicks(sections[headers["Start"]]);
subEvent.EndPositionTicks = GetTicks(sections[headers["End"]]);
- subEvent.Text = string.Join(",", sections.Skip(headers["Text"]));
+ subEvent.Text = string.Join(',', sections[headers["Text"]..]);
RemoteNativeFormatting(subEvent);
subEvent.Text = subEvent.Text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase);
@@ -62,7 +63,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- trackInfo.TrackEvents = trackEvents.ToArray();
+ trackInfo.TrackEvents = trackEvents;
return trackInfo;
}
@@ -72,9 +73,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
? span.Ticks : 0;
}
- private Dictionary<string, int> ParseFieldHeaders(string line)
+ internal static Dictionary<string, int> ParseFieldHeaders(string line)
{
- var fields = line.Substring(8).Split(',').Select(x => x.Trim()).ToList();
+ const string Format = "Format: ";
+ var fields = line.Substring(Format.Length).Split(',').Select(x => x.Trim()).ToList();
return new Dictionary<string, int>
{
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
index cc35efb3f..4a87f87dc 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
@@ -69,7 +69,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var multiline = new List<string>();
while ((line = reader.ReadLine()) != null)
{
- if (string.IsNullOrEmpty(line))
+ if (line.Length == 0)
{
break;
}
@@ -87,7 +87,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- trackInfo.TrackEvents = trackEvents.ToArray();
+ trackInfo.TrackEvents = trackEvents;
return trackInfo;
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
index db6b47583..bc84c5074 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
@@ -325,7 +325,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
text = text.Insert(start, "<font color=\"" + color + "\"" + extraTags + ">");
}
- text += "</font>";
+ int indexOfEndTag = text.IndexOf("{\\1c}", start, StringComparison.Ordinal);
+ if (indexOfEndTag > 0)
+ {
+ text = text.Remove(indexOfEndTag, "{\\1c}".Length).Insert(indexOfEndTag, "</font>");
+ }
+ else
+ {
+ text += "</font>";
+ }
}
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index c271a9cf8..e5166672f 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -35,7 +35,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="System.Globalization" Version="4.3.0" />
- <PackageReference Include="System.Text.Json" Version="5.0.0" />
+ <PackageReference Include="System.Text.Json" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/MediaBrowser.Model/Notifications/NotificationType.cs b/MediaBrowser.Model/Notifications/NotificationType.cs
index d58fbbc21..a8b257b8d 100644
--- a/MediaBrowser.Model/Notifications/NotificationType.cs
+++ b/MediaBrowser.Model/Notifications/NotificationType.cs
@@ -18,7 +18,6 @@ namespace MediaBrowser.Model.Notifications
NewLibraryContent,
ServerRestartRequired,
TaskFailed,
- CameraImageUploaded,
UserLockedOut
}
}
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 4ad336d33..001d0623c 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -64,10 +64,16 @@ namespace MediaBrowser.Model.Querying
public bool EnableTotalRecordCount { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether do disable sending first episode as next up.
+ /// </summary>
+ public bool DisableFirstEpisode { get; set; }
+
public NextUpQuery()
{
EnableImageTypes = Array.Empty<ImageType>();
EnableTotalRecordCount = true;
+ DisableFirstEpisode = false;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 3da999ad0..e3301ff32 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -272,6 +272,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return path;
}
}
+ else
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ }
var url = GetOmdbUrl(
string.Format(
@@ -280,8 +284,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam));
var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- await using FileStream jsonFileStream = File.OpenWrite(path);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
@@ -308,6 +311,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return path;
}
}
+ else
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ }
var url = GetOmdbUrl(
string.Format(
@@ -317,8 +324,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
seasonId));
var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(path));
- await using FileStream jsonFileStream = File.OpenWrite(path);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index c287113c5..f2d0bdc54 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -63,14 +63,14 @@ namespace MediaBrowser.XbmcMetadata.Parsers
/// <exception cref="ArgumentException"><c>metadataFile</c> is <c>null</c> or empty.</exception>
public void Fetch(MetadataResult<T> item, string metadataFile, CancellationToken cancellationToken)
{
- if (item == null)
+ if (item.Item == null)
{
- throw new ArgumentNullException(nameof(item));
+ throw new ArgumentException("Item can't be null.", nameof(item));
}
if (string.IsNullOrEmpty(metadataFile))
{
- throw new ArgumentException("The metadata file was empty or null.", nameof(metadataFile));
+ throw new ArgumentException("The metadata filepath was empty.", nameof(metadataFile));
}
_validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -270,17 +270,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, BaseNfoSaver.DateAddedFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var added))
- {
- item.DateCreated = added.ToUniversalTime();
- }
- else if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out added))
+ if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var added))
{
item.DateCreated = added.ToUniversalTime();
}
else
{
- Logger.LogWarning("Invalid Added value found: " + val);
+ Logger.LogWarning("Invalid Added value found: {Value}", val);
}
}
@@ -299,11 +295,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "name":
case "title":
case "localtitle":
item.Name = reader.ReadElementContentAsString();
break;
+ case "sortname":
+ item.SortName = reader.ReadElementContentAsString();
+ break;
+
case "criticrating":
{
var text = reader.ReadElementContentAsString();
@@ -384,16 +385,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "tagline":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.Tagline = val;
- }
-
- break;
- }
+ item.Tagline = reader.ReadElementContentAsString();
+ break;
case "country":
{
@@ -635,7 +628,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
{
item.PremiereDate = date.ToUniversalTime();
item.ProductionYear = date.Year;
@@ -653,7 +646,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val))
{
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var date) && date.Year > 1850)
+ if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date) && date.Year > 1850)
{
item.EndDate = date.ToUniversalTime();
}
@@ -710,6 +703,38 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "uniqueid":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
+ var provider = reader.GetAttribute("type");
+ var id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(provider, id);
+ }
+
+ break;
+ }
+
+ case "musicBrainzArtistID":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
+ var id = reader.ReadElementContentAsString();
+ item.SetProviderId(MetadataProvider.MusicBrainzArtist.ToString(), id);
+
+ break;
+ }
+
default:
string readerName = reader.Name;
if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
@@ -797,6 +822,22 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "subtitle":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ using (var subtree = reader.ReadSubtree())
+ {
+ FetchFromSubtitleNode(subtree, item);
+ }
+
+ break;
+ }
+
default:
reader.Skip();
break;
@@ -854,6 +895,90 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "aspect":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (item is Video video)
+ {
+ video.AspectRatio = val;
+ }
+
+ break;
+ }
+
+ case "width":
+ {
+ var val = reader.ReadElementContentAsInt();
+
+ if (item is Video video)
+ {
+ video.Width = val;
+ }
+
+ break;
+ }
+
+ case "height":
+ {
+ var val = reader.ReadElementContentAsInt();
+
+ if (item is Video video)
+ {
+ video.Height = val;
+ }
+
+ break;
+ }
+
+ case "durationinseconds":
+ {
+ var val = reader.ReadElementContentAsInt();
+
+ if (item is Video video)
+ {
+ video.RunTimeTicks = new TimeSpan(0, 0, val).Ticks;
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
+ private void FetchFromSubtitleNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "language":
+ {
+ _ = reader.ReadElementContentAsString();
+
+ if (item is Video video)
+ {
+ video.HasSubtitles = true;
+ }
+
+ break;
+ }
+
default:
reader.Skip();
break;
@@ -877,6 +1002,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var type = PersonType.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
+ string? imageUrl = null;
reader.MoveToContent();
reader.Read();
@@ -904,6 +1030,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "order":
case "sortorder":
{
var val = reader.ReadElementContentAsString();
@@ -919,6 +1046,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "thumb":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ imageUrl = val;
+ }
+
+ break;
+ }
+
default:
reader.Skip();
break;
@@ -935,7 +1074,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
Name = name.Trim(),
Role = role,
Type = type,
- SortOrder = sortOrder
+ SortOrder = sortOrder,
+ ImageUrl = imageUrl
};
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index bce4cf009..81774b873 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -208,6 +208,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
+ case "showtitle":
+ {
+ var showtitle = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(showtitle))
+ {
+ item.SeriesName = showtitle;
+ }
+
+ break;
+ }
+
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index 15a2fb63e..33b0ae887 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -75,7 +75,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val) && movie != null)
{
// TODO Handle this better later
- if (val.IndexOf('<', StringComparison.Ordinal) == -1)
+ if (!val.Contains('<', StringComparison.Ordinal))
{
movie.CollectionName = val;
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
index ac2fbb8d2..5d3d17893 100644
--- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs
@@ -56,6 +56,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
var episode = (Episode)item;
+ writer.WriteElementString("showtitle", episode.SeriesName);
+
if (episode.IndexNumber.HasValue)
{
writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(_usCulture));
@@ -122,7 +124,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
"airsbefore_episode",
"airsbefore_season",
"displayseason",
- "displayepisode"
+ "displayepisode",
+ "showtitle"
});
return list;
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index c654e8ef3..4e6687cce 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 10.0.40219.1
@@ -72,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Dlna.Tests", "tests\Jellyfin.Dlna.Tests\Jellyfin.Dlna.Tests.csproj", "{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.XbmcMetadata.Tests", "tests\Jellyfin.XbmcMetadata.Tests\Jellyfin.XbmcMetadata.Tests.csproj", "{30922383-D513-4F4D-B890-A940B57FA353}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -194,6 +196,10 @@ Global
{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50}.Release|Any CPU.Build.0 = Release|Any CPU
+ {30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {30922383-D513-4F4D-B890-A940B57FA353}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {30922383-D513-4F4D-B890-A940B57FA353}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -207,6 +213,7 @@ Global
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{B8AE4B9D-E8D3-4B03-A95E-7FD8CECECC50} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64
index d2f98ca82..f5cf232d6 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index ffc94e088..d9414a610 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index b25f59329..7f2275aaa 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 2e993c25d..54d75dcbe 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index 08b4ffa52..e4c724219 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index b8499c917..633802598 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 80c4d1469..ec0b015cc 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index f2bbe7f24..25f15be18 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index 603becedf..cd71ce9d4 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index a6c7cc5d4..ea539b360 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index 3a8005816..f2f5368f7 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index 22b9e7ea8..ba597801b 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64
index b1ca61053..c73126841 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -15,7 +15,7 @@ RUN apt-get update \
# Install dotnet repository
# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
-RUN wget https://download.visualstudio.microsoft.com/download/pr/a0487784-534a-4912-a4dd-017382083865/be16057043a8f7b6f08c902dc48dd677/dotnet-sdk-5.0.101-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget https://download.visualstudio.microsoft.com/download/pr/7f736160-9f34-4595-8d72-13630c437aef/b9c4513afb0f8872eb95793c70ac52f6/dotnet-sdk-5.0.102-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 197126ee5..71583c24e 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -28,7 +28,7 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel,
# COPR @dotnet-sig/dotnet or
# https://packages.microsoft.com/rhel/7/prod/
BuildRequires: dotnet-runtime-5.0, dotnet-sdk-5.0
-Requires: %{name}-server = %{version}-%{release}, %{name}-web >= 10.6, %{name}-web < 10.7
+Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release}
# Disable Automatic Dependency Processing
AutoReqProv: no
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 45ab725eb..371f02566 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -10,6 +10,8 @@
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
+ <!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
+ <Rule Id="SA1011" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 45c93987b..98a2cda60 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -16,13 +16,13 @@
<PackageReference Include="AutoFixture" Version="4.15.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.15.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
- <PackageReference Include="Moq" Version="4.15.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
+ <PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs
new file mode 100644
index 000000000..6c3fd0ee1
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/ParseNetworkTests.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Globalization;
+using System.Text;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
+using Jellyfin.Server.Extensions;
+using MediaBrowser.Common.Configuration;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Api.Tests
+{
+ public class ParseNetworkTests
+ {
+ /// <summary>
+ /// Order of the result has always got to be hosts, then networks.
+ /// </summary>
+ /// <param name="ip4">IP4 enabled.</param>
+ /// <param name="ip6">IP6 enabled.</param>
+ /// <param name="hostList">List to parse.</param>
+ /// <param name="match">What it should match.</param>
+ [Theory]
+ // [InlineData(true, true, "192.168.0.0/16,www.yahoo.co.uk", "::ffff:212.82.100.150,::ffff:192.168.0.0/16")] <- fails on Max. www.yahoo.co.uk resolves to a different ip address.
+ // [InlineData(true, false, "192.168.0.0/16,www.yahoo.co.uk", "212.82.100.150,192.168.0.0/16")]
+ [InlineData(true, true, "192.168.t,127.0.0.1,1234.1232.12.1234", "::ffff:127.0.0.1")]
+ [InlineData(true, false, "192.168.x,127.0.0.1,1234.1232.12.1234", "127.0.0.1")]
+ [InlineData(true, true, "::1", "::1/128")]
+ public void TestNetworks(bool ip4, bool ip6, string hostList, string match)
+ {
+ using var nm = CreateNetworkManager();
+
+ var settings = new NetworkConfiguration
+ {
+ EnableIPV4 = ip4,
+ EnableIPV6 = ip6
+ };
+
+ var result = match + ',';
+ ForwardedHeadersOptions options = new ForwardedHeadersOptions();
+
+ // Need this here as ::1 and 127.0.0.1 are in them by default.
+ options.KnownProxies.Clear();
+ options.KnownNetworks.Clear();
+
+ ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList.Split(","), options);
+
+ var sb = new StringBuilder();
+ foreach (var item in options.KnownProxies)
+ {
+ sb.Append(item);
+ sb.Append(',');
+ }
+
+ foreach (var item in options.KnownNetworks)
+ {
+ sb.Append(item.Prefix);
+ sb.Append('/');
+ sb.Append(item.PrefixLength.ToString(CultureInfo.InvariantCulture));
+ sb.Append(',');
+ }
+
+ Assert.Equal(sb.ToString(), result);
+ }
+
+ private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
+ {
+ var configManager = new Mock<IConfigurationManager>
+ {
+ CallBase = true
+ };
+ configManager.Setup(x => x.GetConfiguration(It.IsAny<string>())).Returns(conf);
+ return configManager.Object;
+ }
+
+ private static NetworkManager CreateNetworkManager()
+ {
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ };
+
+ return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
+ }
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 19c5612c0..7ff4d0e87 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
index efc0c4af9..22bc7afb9 100644
--- a/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
+++ b/tests/Jellyfin.Common.Tests/Json/JsonNullableGuidConverterTests.cs
@@ -39,18 +39,30 @@ namespace Jellyfin.Common.Tests.Json
}
[Fact]
- public void Deserialize_Null_EmptyGuid()
+ public void Deserialize_Null_Null()
{
Assert.Null(JsonSerializer.Deserialize<Guid?>("null", _options));
}
[Fact]
- public void Serialize_EmptyGuid_EmptyGuid()
+ public void Deserialize_EmptyGuid_EmptyGuid()
+ {
+ Assert.Equal(Guid.Empty, JsonSerializer.Deserialize<Guid?>(@"""00000000-0000-0000-0000-000000000000""", _options));
+ }
+
+ [Fact]
+ public void Serialize_EmptyGuid_Null()
{
Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, _options));
}
[Fact]
+ public void Serialize_Null_Null()
+ {
+ Assert.Equal("null", JsonSerializer.Serialize((Guid?)null, _options));
+ }
+
+ [Fact]
public void Serialize_Valid_NoDash_Success()
{
var guid = (Guid?)new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
diff --git a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
index 929bb92aa..0adf098c3 100644
--- a/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
+++ b/tests/Jellyfin.Controller.Tests/AlphanumComparatorTests.cs
@@ -1,6 +1,5 @@
using System;
using System.Linq;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Sorting;
using Xunit;
@@ -8,8 +7,6 @@ namespace Jellyfin.Controller.Tests
{
public class AlphanumComparatorTests
{
- private readonly Random _rng = new Random(42);
-
// InlineData is pre-sorted
[Theory]
[InlineData(null, "", "1", "9", "10", "a", "z")]
@@ -25,18 +22,7 @@ namespace Jellyfin.Controller.Tests
[InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")]
public void AlphanumComparatorTest(params string?[] strings)
{
- var copy = (string?[])strings.Clone();
- if (strings.Length == 2)
- {
- var tmp = copy[0];
- copy[0] = copy[1];
- copy[1] = tmp;
- }
- else
- {
- copy.Shuffle(_rng);
- }
-
+ var copy = strings.Reverse().ToArray();
Array.Sort(copy, new AlphanumComparator());
Assert.True(strings.SequenceEqual(copy));
}
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index 1ec88dada..af412eed3 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index 8c9dc4820..21389b926 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -11,7 +11,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index c934ea1c2..9a1cfcfa4 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs
new file mode 100644
index 000000000..d11cb242c
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/SsaParserTests.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.MediaInfo;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Tests
+{
+ public class SsaParserTests
+ {
+ // commonly shared invariant value between tests, assumes default format order
+ private const string InvariantDialoguePrefix = "[Events]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,";
+
+ private SsaParser parser = new SsaParser();
+
+ [Theory]
+ [InlineData("[EvEnTs]\nDialogue: ,0:00:00.00,0:00:00.01,,,,,,,text", "text")] // label casing insensitivity
+ [InlineData("[Events]\n,0:00:00.00,0:00:00.01,,,,,,,labelless dialogue", "labelless dialogue")] // no "Dialogue:" label, it is optional
+ [InlineData("[Events]\nFormat: Text, Start, End, Layer, Effect, Style\nDialogue: reordered text,0:00:00.00,0:00:00.01", "reordered text")] // reordered formats
+ [InlineData(InvariantDialoguePrefix + "Cased TEXT", "Cased TEXT")] // preserve text casing
+ [InlineData(InvariantDialoguePrefix + " text ", " text ")] // do not trim text
+ [InlineData(InvariantDialoguePrefix + "text, more text", "text, more text")] // append excess dialogue values (> 10) to text
+ [InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text{\\fn} end", "start <font face=\"Font Name\">text</font> end")] // font name
+ [InlineData(InvariantDialoguePrefix + "start {\\fs10}text{\\fs} end", "start <font size=\"10\">text</font> end")] // font size
+ [InlineData(InvariantDialoguePrefix + "start {\\c&H112233}text{\\c} end", "start <font color=\"#332211\">text</font> end")] // color
+ [InlineData(InvariantDialoguePrefix + "start {\\1c&H112233}text{\\1c} end", "start <font color=\"#332211\">text</font> end")] // primay color
+ [InlineData(InvariantDialoguePrefix + "start {\\fnFont Name}text1 {\\fs10}text2{\\fs}{\\fn} {\\1c&H112233}text3{\\1c} end", "start <font face=\"Font Name\">text1 <font size=\"10\">text2</font></font> <font color=\"#332211\">text3</font> end")] // nested formatting
+ public void Parse(string ssa, string expectedText)
+ {
+ using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
+ {
+ SubtitleTrackInfo subtitleTrackInfo = parser.Parse(stream, CancellationToken.None);
+ SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[0];
+ Assert.Equal(expectedText, actual.Text);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(Parse_MultipleDialogues_TestData))]
+ public void Parse_MultipleDialogues(string ssa, IReadOnlyList<SubtitleTrackEvent> expectedSubtitleTrackEvents)
+ {
+ using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
+ {
+ SubtitleTrackInfo subtitleTrackInfo = parser.Parse(stream, CancellationToken.None);
+
+ Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
+
+ for (int i = 0; i < expectedSubtitleTrackEvents.Count; ++i)
+ {
+ SubtitleTrackEvent expected = expectedSubtitleTrackEvents[i];
+ SubtitleTrackEvent actual = subtitleTrackInfo.TrackEvents[i];
+
+ Assert.Equal(expected.StartPositionTicks, actual.StartPositionTicks);
+ Assert.Equal(expected.EndPositionTicks, actual.EndPositionTicks);
+ Assert.Equal(expected.Text, actual.Text);
+ }
+ }
+ }
+
+ public static IEnumerable<object[]> Parse_MultipleDialogues_TestData()
+ {
+ yield return new object[]
+ {
+ @"[Events]
+ Format: Layer, Start, End, Text
+ Dialogue: ,0:00:01.18,0:00:01.85,dialogue1
+ Dialogue: ,0:00:02.18,0:00:02.85,dialogue2
+ Dialogue: ,0:00:03.18,0:00:03.85,dialogue3
+ ",
+ new List<SubtitleTrackEvent>
+ {
+ new SubtitleTrackEvent
+ {
+ StartPositionTicks = 11800000,
+ EndPositionTicks = 18500000,
+ Text = "dialogue1"
+ },
+ new SubtitleTrackEvent
+ {
+ StartPositionTicks = 21800000,
+ EndPositionTicks = 28500000,
+ Text = "dialogue2"
+ },
+ new SubtitleTrackEvent
+ {
+ StartPositionTicks = 31800000,
+ EndPositionTicks = 38500000,
+ Text = "dialogue3"
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
new file mode 100644
index 000000000..14ad49839
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using MediaBrowser.MediaEncoding.Subtitles;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class AssParserTests
+ {
+ [Fact]
+ public void Parse_Valid_Success()
+ {
+ using (var stream = File.OpenRead("Test Data/example.ass"))
+ {
+ var parsed = new AssParser().Parse(stream, CancellationToken.None);
+ Assert.Single(parsed.TrackEvents);
+ var trackEvent = parsed.TrackEvents[0];
+
+ Assert.Equal("1", trackEvent.Id);
+ Assert.Equal(TimeSpan.Parse("00:00:01.18", CultureInfo.InvariantCulture).Ticks, trackEvent.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:00:06.85", CultureInfo.InvariantCulture).Ticks, trackEvent.EndPositionTicks);
+ Assert.Equal("Like an Angel with pity on nobody\r\nThe second line in subtitle", trackEvent.Text);
+ }
+ }
+
+ [Fact]
+ public void ParseFieldHeaders_Valid_Success()
+ {
+ const string Line = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text";
+ var headers = AssParser.ParseFieldHeaders(Line);
+ Assert.Equal(1, headers["Start"]);
+ Assert.Equal(2, headers["End"]);
+ Assert.Equal(9, headers["Text"]);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
new file mode 100644
index 000000000..3e2d2de10
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using MediaBrowser.MediaEncoding.Subtitles;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class SrtParserTests
+ {
+ [Fact]
+ public void Parse_Valid_Success()
+ {
+ using (var stream = File.OpenRead("Test Data/example.srt"))
+ {
+ var parsed = new SrtParser(new NullLogger<SrtParser>()).Parse(stream, CancellationToken.None);
+ Assert.Equal(2, parsed.TrackEvents.Count);
+
+ var trackEvent1 = parsed.TrackEvents[0];
+ Assert.Equal("1", trackEvent1.Id);
+ Assert.Equal(TimeSpan.Parse("00:02:17.440", CultureInfo.InvariantCulture).Ticks, trackEvent1.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:02:20.375", CultureInfo.InvariantCulture).Ticks, trackEvent1.EndPositionTicks);
+ Assert.Equal("Senator, we're making\r\nour final approach into Coruscant.", trackEvent1.Text);
+
+ var trackEvent2 = parsed.TrackEvents[1];
+ Assert.Equal("2", trackEvent2.Id);
+ Assert.Equal(TimeSpan.Parse("00:02:20.476", CultureInfo.InvariantCulture).Ticks, trackEvent2.StartPositionTicks);
+ Assert.Equal(TimeSpan.Parse("00:02:22.501", CultureInfo.InvariantCulture).Ticks, trackEvent2.EndPositionTicks);
+ Assert.Equal("Very good, Lieutenant.", trackEvent2.Text);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ass b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ass
new file mode 100644
index 000000000..d5ac31d70
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.ass
@@ -0,0 +1,22 @@
+[Script Info]
+; Script generated by Aegisub
+; http://www.aegisub.org
+Title: Neon Genesis Evangelion - Episode 26 (neutral Spanish)
+Original Script: RoRo
+Script Updated By: version 2.8.01
+ScriptType: v4.00+
+Collisions: Normal
+PlayResY: 600
+PlayDepth: 0
+Timer: 100,0000
+Video Aspect Ratio: 0
+Video Zoom: 6
+Video Position: 0
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: DefaultVCD, Arial,28,&H00B4FCFC,&H00B4FCFC,&H00000008,&H80000008,-1,0,0,0,100,100,0.00,0.00,1,1.00,2.00,2,30,30,30,0
+
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: 0,0:00:01.18,0:00:06.85,DefaultVCD, NTP,0000,0000,0000,,{\pos(400,570)}Like an Angel with pity on nobody\NThe second line in subtitle
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.srt b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.srt
new file mode 100644
index 000000000..78d74014e
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/example.srt
@@ -0,0 +1,8 @@
+1
+00:02:17,440 --> 00:02:20,375
+Senator, we're making
+our final approach into Coruscant.
+
+2
+00:02:20,476 --> 00:02:22,501
+Very good, Lieutenant.
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 6118581e1..31e41b12f 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
index 90782f6bb..bb2bae8e8 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
@@ -16,8 +16,8 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
- <PackageReference Include="Moq" Version="4.15.2" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
+ <PackageReference Include="Moq" Version="4.16.0" />
</ItemGroup>
<!-- Code Analyzers-->
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
index c350685af..b7c1510d2 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
@@ -54,13 +54,13 @@ namespace Jellyfin.Networking.Tests
/// <summary>
/// Checks the ability to ignore interfaces
/// </summary>
- /// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) : .... </param>
+ /// <param name="interfaces">Mock network setup, in the format (IP address, interface index, interface name) | .... </param>
/// <param name="lan">LAN addresses.</param>
/// <param name="value">Bind addresses that are excluded.</param>
[Theory]
- [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
- [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
- [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+ [InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
+ [InlineData("192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+ [InlineData("192.168.1.208/24,-16,vEthernet1|192.168.1.208/24,-16,vEthernet212|200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
{
var conf = new NetworkConfiguration()
@@ -434,7 +434,7 @@ namespace Jellyfin.Networking.Tests
EnableIPV4 = true
};
- NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11";
+ NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty;
@@ -501,7 +501,7 @@ namespace Jellyfin.Networking.Tests
PublishedServerUriBySubnet = new string[] { publishedServers }
};
- NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11";
+ NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11";
using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>());
NetworkManager.MockNetworkSettings = string.Empty;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 80259a55f..7f909847e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -17,10 +17,10 @@
<PackageReference Include="AutoFixture" Version="4.15.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.15.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
- <PackageReference Include="Moq" Version="4.15.2" />
+ <PackageReference Include="Moq" Version="4.16.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
index fb7cf6a47..75939526d 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
new file mode 100644
index 000000000..fe40122a3
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -0,0 +1,40 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <IsPackable>false</IsPackable>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+ <PackageReference Include="Moq" Version="4.16.0" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+ <PackageReference Include="coverlet.collector" Version="3.0.1" />
+ </ItemGroup>
+
+ <!-- Code Analyzers -->
+ <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
+ <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
+ <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+</Project>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
new file mode 100644
index 000000000..67b4b969a
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+#pragma warning disable CA5369
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class EpisodeNfoProviderTests
+ {
+ private readonly EpisodeNfoParser _parser;
+
+ public EpisodeNfoProviderTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+ _parser = new EpisodeNfoParser(new NullLogger<EpisodeNfoParser>(), config.Object, providerManager.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<Episode>()
+ {
+ Item = new Episode()
+ };
+
+ _parser.Fetch(result, "Test Data/The Bone Orchard.nfo", CancellationToken.None);
+
+ var item = result.Item;
+ Assert.Equal("The Bone Orchard", item.Name);
+ Assert.Equal("American Gods", item.SeriesName);
+ Assert.Equal(1, item.IndexNumber);
+ Assert.Equal(1, item.ParentIndexNumber);
+ Assert.Equal("When Shadow Moon is released from prison early after the death of his wife, he meets Mr. Wednesday and is recruited as his bodyguard. Shadow discovers that this may be more than he bargained for.", item.Overview);
+ Assert.Equal(0, item.RunTimeTicks);
+ Assert.Equal("16", item.OfficialRating);
+ Assert.Contains("Drama", item.Genres);
+ Assert.Contains("Mystery", item.Genres);
+ Assert.Contains("Sci-Fi & Fantasy", item.Genres);
+ Assert.Equal(new DateTime(2017, 4, 30), item.PremiereDate);
+ Assert.Equal(2017, item.ProductionYear);
+ Assert.Single(item.Studios);
+ Assert.Contains("Starz", item.Studios);
+
+ // Credits
+ var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray();
+ Assert.Equal(2, writers.Length);
+ Assert.Contains("Bryan Fuller", writers.Select(x => x.Name));
+ Assert.Contains("Michael Green", writers.Select(x => x.Name));
+
+ // Direcotrs
+ var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray();
+ Assert.Single(directors);
+ Assert.Contains("David Slade", directors.Select(x => x.Name));
+
+ // Actors
+ var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray();
+ Assert.Equal(11, actors.Length);
+ // Only test one actor
+ var shadow = actors.FirstOrDefault(x => x.Role.Equals("Shadow Moon", StringComparison.Ordinal));
+ Assert.NotNull(shadow);
+ Assert.Equal("Ricky Whittle", shadow!.Name);
+ Assert.Equal(0, shadow!.SortOrder);
+ Assert.Equal("http://image.tmdb.org/t/p/original/cjeDbVfBp6Qvb3C74Dfy7BKDTQN.jpg", shadow!.ImageUrl);
+
+ Assert.Equal(new DateTime(2017, 10, 7, 14, 25, 47), item.DateCreated);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Episode>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/The Bone Orchard.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Episode>()
+ {
+ Item = new Episode()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
new file mode 100644
index 000000000..e1f50876f
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class MovieNfoParserTests
+ {
+ private readonly MovieNfoParser _parser;
+
+ public MovieNfoParserTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+ _parser = new MovieNfoParser(new NullLogger<MovieNfoParser>(), config.Object, providerManager.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new Video()
+ };
+
+ _parser.Fetch(result, "Test Data/Justice League.nfo", CancellationToken.None);
+ var item = result.Item;
+
+ Assert.Equal("Justice League", item.OriginalTitle);
+ Assert.Equal("Justice for all.", item.Tagline);
+ Assert.Equal("tt0974015", item.ProviderIds["imdb"]);
+
+ Assert.Equal(4, item.Genres.Length);
+ Assert.Contains("Action", item.Genres);
+ Assert.Contains("Adventure", item.Genres);
+ Assert.Contains("Fantasy", item.Genres);
+ Assert.Contains("Sci-Fi", item.Genres);
+
+ Assert.Equal(new DateTime(2017, 11, 15), item.PremiereDate);
+ Assert.Single(item.Studios);
+ Assert.Contains("DC Comics", item.Studios);
+
+ Assert.Equal("1.777778", item.AspectRatio);
+ Assert.Equal(1920, item.Width);
+ Assert.Equal(1080, item.Height);
+ Assert.Equal(new TimeSpan(0, 0, 6268).Ticks, item.RunTimeTicks);
+ Assert.True(item.HasSubtitles);
+
+ Assert.Equal(18, result.People.Count);
+
+ var writers = result.People.Where(x => x.Type == PersonType.Writer).ToArray();
+ Assert.Equal(2, writers.Length);
+ var writerNames = writers.Select(x => x.Name);
+ Assert.Contains("Jerry Siegel", writerNames);
+ Assert.Contains("Joe Shuster", writerNames);
+
+ var directors = result.People.Where(x => x.Type == PersonType.Director).ToArray();
+ Assert.Single(directors);
+ Assert.Equal("Zack Snyder", directors[0].Name);
+
+ var actors = result.People.Where(x => x.Type == PersonType.Actor).ToArray();
+ Assert.Equal(15, actors.Length);
+
+ // Only test one actor
+ var aquaman = actors.FirstOrDefault(x => x.Role.Equals("Aquaman", StringComparison.Ordinal));
+ Assert.NotNull(aquaman);
+ Assert.Equal("Jason Momoa", aquaman!.Name);
+ Assert.Equal(5, aquaman!.SortOrder);
+ Assert.Equal("https://m.media-amazon.com/images/M/MV5BMTI5MTU5NjM1MV5BMl5BanBnXkFtZTcwODc4MDk0Mw@@._V1_SX1024_SY1024_.jpg", aquaman!.ImageUrl);
+
+ Assert.Equal(new DateTime(2019, 8, 6, 9, 1, 18), item.DateCreated);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Video>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/Justice League.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Video>()
+ {
+ Item = new Video()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
new file mode 100644
index 000000000..bdffea560
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs
@@ -0,0 +1,72 @@
+#pragma warning disable CA5369
+
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class MusicAlbumNfoProviderTests
+ {
+ private readonly BaseNfoParser<MusicAlbum> _parser;
+
+ public MusicAlbumNfoProviderTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+ _parser = new BaseNfoParser<MusicAlbum>(new NullLogger<BaseNfoParser<MusicAlbum>>(), config.Object, providerManager.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<MusicAlbum>()
+ {
+ Item = new MusicAlbum()
+ };
+
+ _parser.Fetch(result, "Test Data/The Best of 1980-1990.nfo", CancellationToken.None);
+ var item = result.Item;
+
+ Assert.Equal("The Best of 1980-1990", item.Name);
+ Assert.Equal(1989, item.ProductionYear);
+ Assert.Contains("Pop", item.Genres);
+ Assert.Single(item.Genres);
+ Assert.Contains("Rock/Pop", item.Tags);
+ Assert.Equal("The Best of 1980-1990 is the first greatest hits compilation by Irish rock band U2, released in November 1998. It mostly contains the group's hit singles from the eighties but also mixes in some live staples as well as one new recording, Sweetest Thing. In April 1999, a companion video (featuring music videos and live footage) was released. The album was followed by another compilation, The Best of 1990-2000, in 2002.\nA limited edition version containing a special B-sides disc was released on the same date as the single-disc version. At the time of release, the official word was that the 2-disc album would be available the first week the album went on sale, then pulled from the stores. While this threat never materialized, it did result in the 2-disc version being in very high demand. Both versions charted in the Billboard 200.\nThe boy on the cover is Peter Rowan, brother of Bono's friend Guggi (real name Derek Rowan) of the Virgin Prunes. He also appears on the covers of the early EP Three, two of the band's first three albums (Boy and War), and Early Demos.", item.Overview);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<MusicAlbum>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/The Best of 1980-1990.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<MusicAlbum>()
+ {
+ Item = new MusicAlbum()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
new file mode 100644
index 000000000..2a4d376c6
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class MusicArtistNfoParserTests
+ {
+ private readonly BaseNfoParser<MusicArtist> _parser;
+
+ public MusicArtistNfoParserTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+ _parser = new BaseNfoParser<MusicArtist>(new NullLogger<BaseNfoParser<MusicArtist>>(), config.Object, providerManager.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<MusicArtist>()
+ {
+ Item = new MusicArtist()
+ };
+
+ _parser.Fetch(result, "Test Data/U2.nfo", CancellationToken.None);
+ var item = result.Item;
+
+ Assert.Equal("U2", item.Name);
+ Assert.Equal("U2", item.SortName);
+ Assert.Equal("a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432", item.ProviderIds[MetadataProvider.MusicBrainzArtist.ToString()]);
+
+ Assert.Single(item.Genres);
+ Assert.Equal("Rock", item.Genres[0]);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<MusicArtist>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/U2.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<MusicArtist>()
+ {
+ Item = new MusicArtist()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
new file mode 100644
index 000000000..68b7239d2
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeasonNfoProviderTests.cs
@@ -0,0 +1,83 @@
+#pragma warning disable CA5369
+
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class SeasonNfoProviderTests
+ {
+ private readonly SeasonNfoParser _parser;
+
+ public SeasonNfoProviderTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+ _parser = new SeasonNfoParser(new NullLogger<SeasonNfoParser>(), config.Object, providerManager.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<Season>()
+ {
+ Item = new Season()
+ };
+
+ _parser.Fetch(result, "Test Data/Season 01.nfo", CancellationToken.None);
+ var item = result.Item;
+
+ Assert.Equal("Season 1", item.Name);
+ Assert.Equal(1, item.IndexNumber);
+ Assert.False(item.IsLocked);
+ Assert.Equal(2019, item.ProductionYear);
+ Assert.Equal(new DateTime(2019, 11, 08), item.PremiereDate);
+ Assert.Equal(new DateTime(2020, 06, 14, 17, 26, 51), item.DateCreated);
+
+ Assert.Equal(10, result.People.Count);
+
+ Assert.True(result.People.All(x => x.Type == PersonType.Actor));
+
+ // Only test one actor
+ var nini = result.People.FirstOrDefault(x => x.Role.Equals("Nini", StringComparison.Ordinal));
+ Assert.NotNull(nini);
+ Assert.Equal("Olivia Rodrigo", nini!.Name);
+ Assert.Equal(0, nini!.SortOrder);
+ Assert.Equal("/config/metadata/People/O/Olivia Rodrigo/poster.jpg", nini!.ImageUrl);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Season>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/Season 01.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Season>()
+ {
+ Item = new Season()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
new file mode 100644
index 000000000..3bbfb66e3
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.XbmcMetadata.Parsers;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.XbmcMetadata.Tests.Parsers
+{
+ public class SeriesNfoParserTests
+ {
+ private readonly SeriesNfoParser _parser;
+
+ public SeriesNfoParserTests()
+ {
+ var providerManager = new Mock<IProviderManager>();
+ providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny<IHasProviderIds>()))
+ .Returns(Enumerable.Empty<ExternalIdInfo>());
+ var config = new Mock<IConfigurationManager>();
+ config.Setup(x => x.GetConfiguration(It.IsAny<string>()))
+ .Returns(new XbmcMetadataOptions());
+ _parser = new SeriesNfoParser(new NullLogger<SeriesNfoParser>(), config.Object, providerManager.Object);
+ }
+
+ [Fact]
+ public void Fetch_Valid_Succes()
+ {
+ var result = new MetadataResult<Series>()
+ {
+ Item = new Series()
+ };
+
+ _parser.Fetch(result, "Test Data/American Gods.nfo", CancellationToken.None);
+ var item = result.Item;
+
+ Assert.Equal("American Gods", item.OriginalTitle);
+ Assert.Equal(string.Empty, item.Tagline);
+ Assert.Equal(0, item.RunTimeTicks);
+ Assert.Equal("46639", item.ProviderIds["tmdb"]);
+ Assert.Equal("253573", item.ProviderIds["tvdb"]);
+
+ Assert.Equal(3, item.Genres.Length);
+ Assert.Contains("Drama", item.Genres);
+ Assert.Contains("Mystery", item.Genres);
+ Assert.Contains("Sci-Fi & Fantasy", item.Genres);
+
+ Assert.Equal(new DateTime(2017, 4, 30), item.PremiereDate);
+ Assert.Single(item.Studios);
+ Assert.Contains("Starz", item.Studios);
+
+ Assert.Equal(6, result.People.Count);
+
+ Assert.True(result.People.All(x => x.Type == PersonType.Actor));
+
+ // Only test one actor
+ var sweeney = result.People.FirstOrDefault(x => x.Role.Equals("Mad Sweeney", StringComparison.Ordinal));
+ Assert.NotNull(sweeney);
+ Assert.Equal("Pablo Schreiber", sweeney!.Name);
+ Assert.Equal(3, sweeney!.SortOrder);
+ Assert.Equal("http://image.tmdb.org/t/p/original/uo8YljeePz3pbj7gvWXdB4gOOW4.jpg", sweeney!.ImageUrl);
+
+ Assert.Equal(new DateTime(2017, 10, 7, 14, 25, 47), item.DateCreated);
+ }
+
+ [Fact]
+ public void Fetch_WithNullItem_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Series>();
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, "Test Data/American Gods.nfo", CancellationToken.None));
+ }
+
+ [Fact]
+ public void Fetch_NullResult_ThrowsArgumentException()
+ {
+ var result = new MetadataResult<Series>()
+ {
+ Item = new Series()
+ };
+
+ Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
+ }
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo
new file mode 100644
index 000000000..b9f31f2f6
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/American Gods.nfo
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<tvshow>
+ <title>American Gods</title>
+ <originaltitle>American Gods</originaltitle>
+ <showtitle>American Gods</showtitle>
+ <sorttitle>American Gods</sorttitle>
+ <ratings>
+ <rating name="themoviedb" max="10" default="true">
+ <value>6.800000</value>
+ <votes>581</votes>
+ </rating>
+ <rating name="imdb" max="10" default="true">
+ <value>5.500000</value>
+ <votes>86352</votes>
+ </rating>
+ <rating name="metacritic" max="10">
+ <value>6.0</value>
+ <votes>22</votes>
+ </rating>
+ <rating name="tomatometerallcritics" max="10">
+ <value>7.6</value>
+ <votes>71</votes>
+ </rating>
+ <rating name="tomatometerallaudience" max="10">
+ <value>6.2</value>
+ <votes>119873</votes>
+ </rating>
+ </ratings>
+ <userrating>0</userrating>
+ <top250>0</top250>
+ <season>2</season>
+ <episode>16</episode>
+ <displayseason>-1</displayseason>
+ <displayepisode>-1</displayepisode>
+ <outline></outline>
+ <plot>An ex-con becomes the traveling partner of a conman who turns out to be one of the older gods trying to recruit troops to battle the upstart deities. Based on Neil Gaiman&apos;s fantasy novel.</plot>
+ <tagline></tagline>
+ <runtime>0</runtime>
+ <thumb aspect="poster" preview="https://assets.fanart.tv/preview/tv/253573/tvposter/american-gods-58b18cd8d667a.jpg">https://assets.fanart.tv/fanart/tv/253573/tvposter/american-gods-58b18cd8d667a.jpg</thumb>
+ <thumb aspect="poster" preview="https://assets.fanart.tv/preview/tv/253573/tvposter/american-gods-5c896dbee9d21.jpg">https://assets.fanart.tv/fanart/tv/253573/tvposter/american-gods-5c896dbee9d21.jpg</thumb>
+ <thumb aspect="poster" preview="https://assets.fanart.tv/preview/tv/253573/tvposter/american-gods-57dda913a44e0.jpg">https://assets.fanart.tv/fanart/tv/253573/tvposter/american-gods-57dda913a44e0.jpg</thumb>
+ <thumb aspect="poster" preview="https://assets.fanart.tv/preview/tv/253573/tvposter/american-gods-590c159dcbf3a.jpg">https://assets.fanart.tv/fanart/tv/253573/tvposter/american-gods-590c159dcbf3a.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/tv/253573/tvbanner/american-gods-5cbbdaa84298d.jpg">https://assets.fanart.tv/fanart/tv/253573/tvbanner/american-gods-5cbbdaa84298d.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/tv/253573/tvbanner/american-gods-5932b1ffb3522.jpg">https://assets.fanart.tv/fanart/tv/253573/tvbanner/american-gods-5932b1ffb3522.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/tv/253573/tvbanner/american-gods-5932b1ffb43e4.jpg">https://assets.fanart.tv/fanart/tv/253573/tvbanner/american-gods-5932b1ffb43e4.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/tv/253573/tvthumb/american-gods-58db45dc886f5.jpg">https://assets.fanart.tv/fanart/tv/253573/tvthumb/american-gods-58db45dc886f5.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/tv/253573/tvthumb/american-gods-5932aee79947a.jpg">https://assets.fanart.tv/fanart/tv/253573/tvthumb/american-gods-5932aee79947a.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/tv/253573/tvthumb/american-gods-5932aee799e5a.jpg">https://assets.fanart.tv/fanart/tv/253573/tvthumb/american-gods-5932aee799e5a.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/tv/253573/tvthumb/american-gods-5932aee79a2f2.jpg">https://assets.fanart.tv/fanart/tv/253573/tvthumb/american-gods-5932aee79a2f2.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/tv/253573/tvthumb/american-gods-5932aee79a7c9.jpg">https://assets.fanart.tv/fanart/tv/253573/tvthumb/american-gods-5932aee79a7c9.jpg</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/tv/253573/hdtvlogo/american-gods-58b04bdcecefd.png">https://assets.fanart.tv/fanart/tv/253573/hdtvlogo/american-gods-58b04bdcecefd.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/tv/253573/hdtvlogo/american-gods-58b04d78a7ffc.png">https://assets.fanart.tv/fanart/tv/253573/hdtvlogo/american-gods-58b04d78a7ffc.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/tv/253573/hdtvlogo/american-gods-59e6660cb7dbc.png">https://assets.fanart.tv/fanart/tv/253573/hdtvlogo/american-gods-59e6660cb7dbc.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/tv/253573/hdtvlogo/american-gods-59e6660cc0716.png">https://assets.fanart.tv/fanart/tv/253573/hdtvlogo/american-gods-59e6660cc0716.png</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/tv/253573/hdclearart/american-gods-59177740ba6cd.png">https://assets.fanart.tv/fanart/tv/253573/hdclearart/american-gods-59177740ba6cd.png</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/tv/253573/hdclearart/american-gods-5913b6b2ce91d.png">https://assets.fanart.tv/fanart/tv/253573/hdclearart/american-gods-5913b6b2ce91d.png</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/tv/253573/hdclearart/american-gods-5913b6b2cfa64.png">https://assets.fanart.tv/fanart/tv/253573/hdclearart/american-gods-5913b6b2cfa64.png</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/tv/253573/hdclearart/american-gods-5913b6b2cf502.png">https://assets.fanart.tv/fanart/tv/253573/hdclearart/american-gods-5913b6b2cf502.png</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/tv/253573/hdclearart/american-gods-5a4805be0619f.png">https://assets.fanart.tv/fanart/tv/253573/hdclearart/american-gods-5a4805be0619f.png</thumb>
+ <thumb aspect="characterart" preview="https://assets.fanart.tv/preview/tv/253573/characterart/american-gods-5a4805af07a04.png">https://assets.fanart.tv/fanart/tv/253573/characterart/american-gods-5a4805af07a04.png</thumb>
+ <thumb aspect="characterart" preview="https://assets.fanart.tv/preview/tv/253573/characterart/american-gods-59e6b1c71b65a.png">https://assets.fanart.tv/fanart/tv/253573/characterart/american-gods-59e6b1c71b65a.png</thumb>
+ <thumb aspect="poster" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-5d1274a8c31cb.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-5d1274a8c31cb.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-59fea294b565f.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-59fea294b565f.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-5cacdf37068db.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-5cacdf37068db.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-5cacdf7783e04.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-5cacdf7783e04.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-5d1274a8c31cb.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-5d1274a8c31cb.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-59fea294b565f.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-59fea294b565f.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-5cacdf37068db.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-5cacdf37068db.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonposter/american-gods-5cacdf7783e04.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonposter/american-gods-5cacdf7783e04.jpg</thumb>
+ <thumb aspect="banner" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonbanner/american-gods-5cc6b35699d26.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonbanner/american-gods-5cc6b35699d26.jpg</thumb>
+ <thumb aspect="banner" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonbanner/american-gods-5cc6b36965b54.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonbanner/american-gods-5cc6b36965b54.jpg</thumb>
+ <thumb aspect="banner" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonbanner/american-gods-5cc6b35699d26.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonbanner/american-gods-5cc6b35699d26.jpg</thumb>
+ <thumb aspect="banner" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonbanner/american-gods-5cc6b36965b54.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonbanner/american-gods-5cc6b36965b54.jpg</thumb>
+ <thumb aspect="landscape" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonthumb/american-gods-5cc6b380d6c56.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonthumb/american-gods-5cc6b380d6c56.jpg</thumb>
+ <thumb aspect="landscape" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonthumb/american-gods-59e6b5a03e7aa.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonthumb/american-gods-59e6b5a03e7aa.jpg</thumb>
+ <thumb aspect="landscape" type="season" season="2" preview="https://assets.fanart.tv/preview/tv/253573/seasonthumb/american-gods-5cc6b380d6c56.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonthumb/american-gods-5cc6b380d6c56.jpg</thumb>
+ <thumb aspect="landscape" type="season" season="1" preview="https://assets.fanart.tv/preview/tv/253573/seasonthumb/american-gods-59e6b5a03e7aa.jpg">https://assets.fanart.tv/fanart/tv/253573/seasonthumb/american-gods-59e6b5a03e7aa.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/m6qf6lq3yARgbZwspvDLbUFtASh.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/gevw5nZRYz2kWj1PqW9pz4sgeeZ.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/btwTe5cQbGWGOErBiRqnjNP9cJl.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/loJ4sfr4zp995qMoeCHiIIGaOg8.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/dHo8Lw7ruIaQTdTTDZPCMyZxwy5.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/zfAXP4bG2G17VuLNU9cqRcVU0xj.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/oxYUbNpG2st2zXWzYRvewehmvuj.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/mwoQ6zynu2DBxKCBYi30qoM236N.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/8XEoXAMzgcf7m1KiUDZ9N1UGh4o.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/rWsayJB1grML2LdPjjKDC3g0Brr.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/8qRsj8uJ4zPARQmQ9FvejTY1lnV.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/acjnZP0GrwWDxCxV6QejKizbzOy.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/hN1sI57QILGfdrEOqpUfo0NtHjW.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/hz2jNy3DfseYzRSybGRlUtz4pTi.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/hLDgNDdrkB0oWiuClpxN4E3XadJ.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/4FiqawHsVz1mYCRudPtXKbfmP4M.jpg</thumb>
+ <thumb aspect="poster">http://image.tmdb.org/t/p/original/sKR8Q36YBtyRc19y4yGYuD1xBgA.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/4l8Vnbb7e5QA6bAItMqQIHXLRgc.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/ni0thXw5Zi5dQKBY6Oj0vcfIS2n.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/v17HfCzWKQKOBrww9RxZmN5R9tF.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/2ffvlgYsxbXGiWkc3V6Q8tgpiBo.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1">http://image.tmdb.org/t/p/original/rASj7OUjWDhfhAeO2MaFOA3lJpQ.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1">http://image.tmdb.org/t/p/original/67exRijfvN5RRmBCqFtk1bhJ7Uh.jpg</thumb>
+ <thumb aspect="poster" type="season" season="1">http://image.tmdb.org/t/p/original/59iE3xxP7H8rAiXW6TDR2HSoUUm.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/4l8Vnbb7e5QA6bAItMqQIHXLRgc.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/ni0thXw5Zi5dQKBY6Oj0vcfIS2n.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/v17HfCzWKQKOBrww9RxZmN5R9tF.jpg</thumb>
+ <thumb aspect="poster" type="season" season="2">http://image.tmdb.org/t/p/original/2ffvlgYsxbXGiWkc3V6Q8tgpiBo.jpg</thumb>
+ <thumb aspect="banner">https://thetvdb.com/banners/graphical/253573-g3.jpg</thumb>
+ <thumb aspect="banner">https://thetvdb.com/banners/graphical/253573-g4.jpg</thumb>
+ <thumb aspect="banner">https://thetvdb.com/banners/graphical/253573-g2.jpg</thumb>
+ <thumb aspect="banner">https://thetvdb.com/banners/graphical/253573-g.jpg</thumb>
+ <thumb aspect="banner">https://thetvdb.com/banners/graphical/253573-g5.jpg</thumb>
+ <fanart>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5c8965c58e778.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5c8965c58e778.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-59e6a8a495c2a.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-59e6a8a495c2a.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-59e6b13827ba2.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-59e6b13827ba2.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e07ad.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e07ad.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e2913.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e2913.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e0000.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e0000.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e0d3a.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e0d3a.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e1395.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e1395.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e1952.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e1952.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/tv/253573/showbackground/american-gods-5932b089e23ca.jpg">https://assets.fanart.tv/fanart/tv/253573/showbackground/american-gods-5932b089e23ca.jpg</thumb>
+ </fanart>
+ <mpaa>Australia:MA</mpaa>
+ <playcount>0</playcount>
+ <lastplayed></lastplayed>
+ <episodeguide>
+ <url cache="tmdb-46639-en.json">http://api.themoviedb.org/3/tv/46639?api_key=6a5be4999abf74eba1f9a8311294c267&amp;language=en</url>
+ </episodeguide>
+ <id>46639</id>
+ <uniqueid type="tmdb" default="true">46639</uniqueid>
+ <uniqueid type="tvdb">253573</uniqueid>
+ <genre>Drama</genre>
+ <genre>Mystery</genre>
+ <genre>Sci-Fi &amp; Fantasy</genre>
+ <premiered>2017-04-30</premiered>
+ <year>2017</year>
+ <status></status>
+ <code></code>
+ <aired></aired>
+ <studio>Starz</studio>
+ <trailer></trailer>
+ <actor>
+ <name>Ricky Whittle</name>
+ <role>Shadow Moon</role>
+ <order>0</order>
+ <thumb>http://image.tmdb.org/t/p/original/cjeDbVfBp6Qvb3C74Dfy7BKDTQN.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Ian McShane</name>
+ <role>Mr. Wednesday</role>
+ <order>1</order>
+ <thumb>http://image.tmdb.org/t/p/original/pY9ud4BJwHekNiO4MMItPbgkdAy.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Emily Browning</name>
+ <role>Laura Moon</role>
+ <order>2</order>
+ <thumb>http://image.tmdb.org/t/p/original/fa1Kyj02wxwcdS6EHb2i27TNXvU.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Pablo Schreiber</name>
+ <role>Mad Sweeney</role>
+ <order>3</order>
+ <thumb>http://image.tmdb.org/t/p/original/uo8YljeePz3pbj7gvWXdB4gOOW4.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Bruce Langley</name>
+ <role>Technical Boy</role>
+ <order>4</order>
+ <thumb>http://image.tmdb.org/t/p/original/f4EOWUmznLqboq8Ce7jnlkHVK3Y.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Yetide Badaki</name>
+ <role>Bilquis</role>
+ <order>5</order>
+ <thumb>http://image.tmdb.org/t/p/original/qfzkREHuI1JvMxBteIAjKX8qMEr.jpg</thumb>
+ </actor>
+ <namedseason number="1">Season 1</namedseason>
+ <namedseason number="2">Season 2</namedseason>
+ <resume>
+ <position>0.000000</position>
+ <total>0.000000</total>
+ </resume>
+ <dateadded>2017-10-07 14:25:47</dateadded>
+</tvshow>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo
new file mode 100644
index 000000000..f838af8d0
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Justice League.nfo
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<movie>
+ <title>Justice League</title>
+ <originaltitle>Justice League</originaltitle>
+ <ratings>
+ <rating name="imdb" max="10" default="true">
+ <value>6.400000</value>
+ <votes>335583</votes>
+ </rating>
+ <rating name="metacritic" max="10">
+ <value>4.500000</value>
+ <votes>52</votes>
+ </rating>
+ <rating name="themoviedb" max="10">
+ <value>6.200000</value>
+ <votes>7788</votes>
+ </rating>
+ <rating name="tomatometerallcritics" max="10">
+ <value>7.6</value>
+ <votes>71</votes>
+ </rating>
+ <rating name="tomatometerallaudience" max="10">
+ <value>6.2</value>
+ <votes>119873</votes>
+ </rating>
+ </ratings>
+ <userrating>0</userrating>
+ <top250>0</top250>
+ <outline>Fueled by his restored faith in humanity and inspired by Superman&apos;s selfless act, Bruce Wayne enlists the help of his new-found ally, Diana Prince, to face an even greater enemy.</outline>
+ <plot>Fueled by his restored faith in humanity and inspired by Superman&apos;s selfless act, Bruce Wayne enlists the help of his newfound ally, Diana Prince, to face an even greater enemy. Together, Batman and Wonder Woman work quickly to find and recruit a team of meta-humans to stand against this newly awakened threat. But despite the formation of this unprecedented league of heroes-Batman, Wonder Woman, Aquaman, Cyborg and The Flash-it may already be too late to save the planet from an assault of catastrophic proportions.</plot>
+ <tagline>Justice for all.</tagline>
+ <runtime>120</runtime>
+ <thumb aspect="set.poster" preview="https://assets.fanart.tv/preview/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg">https://assets.fanart.tv/fanart/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg</thumb>
+ <thumb aspect="set.poster" preview="https://assets.fanart.tv/preview/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg">https://assets.fanart.tv/fanart/movies/468551/movieposter/justice-league-collection-5c24ea65591d3.jpg</thumb>
+ <thumb aspect="set.clearlogo" preview="https://assets.fanart.tv/preview/movies/468551/hdmovielogo/justice-league-collection-5ba855ed4239a.png">https://assets.fanart.tv/fanart/movies/468551/hdmovielogo/justice-league-collection-5ba855ed4239a.png</thumb>
+ <thumb aspect="set.clearart" preview="https://assets.fanart.tv/preview/movies/468551/hdmovieclearart/justice-league-collection-5c24eae8d4d71.png">https://assets.fanart.tv/fanart/movies/468551/hdmovieclearart/justice-league-collection-5c24eae8d4d71.png</thumb>
+ <thumb aspect="set.landscape" preview="https://assets.fanart.tv/preview/movies/468551/moviethumb/justice-league-collection-5c24ebc7d0d2b.jpg">https://assets.fanart.tv/fanart/movies/468551/moviethumb/justice-league-collection-5c24ebc7d0d2b.jpg</thumb>
+ <thumb aspect="set.poster" preview="http://image.tmdb.org/t/p/w500/cigoYpXWgYYgsIsEPwMneVYpuwo.jpg">http://image.tmdb.org/t/p/original/cigoYpXWgYYgsIsEPwMneVYpuwo.jpg</thumb>
+ <thumb aspect="set.poster" preview="http://image.tmdb.org/t/p/w500/2ZOCiOdAOVSKsFjC1Vl2ZlANrPj.jpg">http://image.tmdb.org/t/p/original/2ZOCiOdAOVSKsFjC1Vl2ZlANrPj.jpg</thumb>
+ <thumb aspect="set.poster" preview="http://image.tmdb.org/t/p/w500/cigoYpXWgYYgsIsEPwMneVYpuwo.jpg">http://image.tmdb.org/t/p/original/cigoYpXWgYYgsIsEPwMneVYpuwo.jpg</thumb>
+ <thumb aspect="set.poster" preview="http://image.tmdb.org/t/p/w500/2ZOCiOdAOVSKsFjC1Vl2ZlANrPj.jpg">http://image.tmdb.org/t/p/original/2ZOCiOdAOVSKsFjC1Vl2ZlANrPj.jpg</thumb>
+ <thumb aspect="set.poster" preview="http://image.tmdb.org/t/p/w500/7iO1C4c5igXsB6AyxnqsCPv6fiq.jpg">http://image.tmdb.org/t/p/original/7iO1C4c5igXsB6AyxnqsCPv6fiq.jpg</thumb>
+ <thumb aspect="set.fanart" preview="http://image.tmdb.org/t/p/w500/vyxOJuk6cxrRcGzuMRbDTpwji1w.jpg">http://image.tmdb.org/t/p/original/vyxOJuk6cxrRcGzuMRbDTpwji1w.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/9rtrRGeRnL0JKtu9IMBWsmlmmZz.jpg">http://image.tmdb.org/t/p/original/9rtrRGeRnL0JKtu9IMBWsmlmmZz.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/eRoXqOzciHkSPs1Z8pGnJMZo0Zb.jpg">http://image.tmdb.org/t/p/original/eRoXqOzciHkSPs1Z8pGnJMZo0Zb.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/aQkEdqaXVxYObMLeoBSAUcgkxLs.jpg">http://image.tmdb.org/t/p/original/aQkEdqaXVxYObMLeoBSAUcgkxLs.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/4lo1fTexk2eNIeQx3Tp74kN7ASE.jpg">http://image.tmdb.org/t/p/original/4lo1fTexk2eNIeQx3Tp74kN7ASE.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/fF94rvT1kJpVzIE2aSYgYj9B3pc.jpg">http://image.tmdb.org/t/p/original/fF94rvT1kJpVzIE2aSYgYj9B3pc.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/uwegp70cWe16EtwsSjbL6ShPenG.jpg">http://image.tmdb.org/t/p/original/uwegp70cWe16EtwsSjbL6ShPenG.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg">http://image.tmdb.org/t/p/original/exLtrlI7JjKcfQVTccI7XdQRFMz.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/paLcue01KpfQftorfjKqqD4qvlL.jpg">http://image.tmdb.org/t/p/original/paLcue01KpfQftorfjKqqD4qvlL.jpg</thumb>
+ <thumb aspect="poster" preview="http://image.tmdb.org/t/p/w500/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg">http://image.tmdb.org/t/p/original/yVDIfiKIsCbdFcgLXW34bAsnQvy.jpg</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5865bf95cbadb.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-585e9ca3bcf6a.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57b476a831d74.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57b476a831d74.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-57947e28cf10b.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-57947e28cf10b.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5863d5c0cf0c9.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5863d5c0cf0c9.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5a801747e5545.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5a801747e5545.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/movies/141052/hdmovielogo/justice-league-5cd75683df92b.png">https://assets.fanart.tv/fanart/movies/141052/hdmovielogo/justice-league-5cd75683df92b.png</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-586017e95adbd.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-586017e95adbd.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-5934d45bc6592.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-5934d45bc6592.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/movies/141052/moviebanner/justice-league-5aa9289a379fa.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebanner/justice-league-5aa9289a379fa.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-585fb155c3743.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585fb155c3743.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-585edbda91d82.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-585edbda91d82.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-5b86588882c12.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-5b86588882c12.jpg</thumb>
+ <thumb aspect="landscape" preview="https://assets.fanart.tv/preview/movies/141052/moviethumb/justice-league-5bbb9babe600c.jpg">https://assets.fanart.tv/fanart/movies/141052/moviethumb/justice-league-5bbb9babe600c.jpg</thumb>
+ <thumb aspect="clearart" preview="https://assets.fanart.tv/preview/movies/141052/hdmovieclearart/justice-league-5865c23193041.png">https://assets.fanart.tv/fanart/movies/141052/hdmovieclearart/justice-league-5865c23193041.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a3af26360617.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a3af26360617.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-58690967b9765.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-58690967b9765.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a953ca4db6a6.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a953ca4db6a6.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a0b913c233be.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a0b913c233be.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-5a87e0cdb1209.png</thumb>
+ <thumb aspect="discart" preview="https://assets.fanart.tv/preview/movies/141052/moviedisc/justice-league-59dc595362ef1.png">https://assets.fanart.tv/fanart/movies/141052/moviedisc/justice-league-59dc595362ef1.png</thumb>
+ <fanart>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5793f518c6d6e.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a5332c7b5e77.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a53cf2dac1c8.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5976ba93eb5d3.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-58fa1f1932897.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-58fa1f1932897.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a14f5fd8dd16.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a14f5fd8dd16.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/movies/141052/moviebackground/justice-league-5a119394ea362.jpg">https://assets.fanart.tv/fanart/movies/141052/moviebackground/justice-league-5a119394ea362.jpg</thumb>
+ </fanart>
+ <mpaa>Australia:M</mpaa>
+ <playcount>0</playcount>
+ <lastplayed></lastplayed>
+ <id>tt0974015</id>
+ <uniqueid type="imdb" default="true">tt0974015</uniqueid>
+ <genre>Action</genre>
+ <genre>Adventure</genre>
+ <genre>Fantasy</genre>
+ <genre>Sci-Fi</genre>
+ <country>USA</country>
+ <country>Canada</country>
+ <country>UK</country>
+ <set>
+ <name>Justice League Collection</name>
+ <overview>Based on the DC Comics superhero team</overview>
+ </set>
+ <credits>Jerry Siegel</credits>
+ <credits>Joe Shuster</credits>
+ <director>Zack Snyder</director>
+ <premiered>2017-11-15</premiered>
+ <year>2017</year>
+ <status></status>
+ <code></code>
+ <aired></aired>
+ <studio>DC Comics</studio>
+ <trailer></trailer>
+ <fileinfo>
+ <streamdetails>
+ <video>
+ <codec>h264</codec>
+ <aspect>1.777778</aspect>
+ <width>1920</width>
+ <height>1080</height>
+ <durationinseconds>6268</durationinseconds>
+ <stereomode></stereomode>
+ </video>
+ <audio>
+ <codec>truehd</codec>
+ <language>eng</language>
+ <channels>8</channels>
+ </audio>
+ <audio>
+ <codec>ac3</codec>
+ <language></language>
+ <channels>6</channels>
+ </audio>
+ <subtitle>
+ <language>eng</language>
+ </subtitle>
+ </streamdetails>
+ </fileinfo>
+ <actor>
+ <name>Ben Affleck</name>
+ <role>Batman</role>
+ <order>0</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTI4MzIxMTk0Nl5BMl5BanBnXkFtZTcwOTU5NjA0Mg@@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Henry Cavill</name>
+ <role>Superman</role>
+ <order>1</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTUxNTExMzUzOF5BMl5BanBnXkFtZTgwOTI1MjA3OTE@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Amy Adams</name>
+ <role>Lois Lane</role>
+ <order>2</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTg2NTk2MTgxMV5BMl5BanBnXkFtZTgwNjcxMjAzMTI@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Gal Gadot</name>
+ <role>Wonder Woman</role>
+ <order>3</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMjUzZTJmZDItODRjYS00ZGRhLTg2NWQtOGE0YjJhNWVlMjNjXkEyXkFqcGdeQXVyMTg4NDI0NDM@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Ezra Miller</name>
+ <role>The Flash</role>
+ <order>4</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMjEwMjQ3ODgxOV5BMl5BanBnXkFtZTgwNzc4NjE4NTE@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Jason Momoa</name>
+ <role>Aquaman</role>
+ <order>5</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTI5MTU5NjM1MV5BMl5BanBnXkFtZTcwODc4MDk0Mw@@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Ray Fisher</name>
+ <role>Cyborg</role>
+ <order>6</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BYzdhMzkyYTgtMjQzMC00ODhmLWExZmItNTU4MDVlMzY2NzgwXkEyXkFqcGdeQXVyNzA5NjQ5MDk@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Jeremy Irons</name>
+ <role>Alfred</role>
+ <order>7</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTY5Mzg2NDY5OV5BMl5BanBnXkFtZTcwMDQwNzA0Mg@@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Diane Lane</name>
+ <role>Martha Kent</role>
+ <order>8</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMzM5ODM1ZWMtZjcyYy00MzgzLWJmMGQtZWY5OGQyNTRiODIxXkEyXkFqcGdeQXVyOTE0NjgwMjY@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Connie Nielsen</name>
+ <role>Queen Hippolyta</role>
+ <order>9</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BYzZiYTQ4YTAtMzRkMi00ZDZlLWFkZWItNGI2ZTIyODRiYTc4XkEyXkFqcGdeQXVyMjUzMjc2MjE@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>J.K. Simmons</name>
+ <role>Commissioner Gordon</role>
+ <order>10</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMzg2NTI5NzQ1MV5BMl5BanBnXkFtZTgwNjI1NDEwMDI@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Ciarán Hinds</name>
+ <role>Steppenwolf</role>
+ <order>11</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTIyNjM0MzU0NF5BMl5BanBnXkFtZTcwOTIxMzg1MQ@@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Amber Heard</name>
+ <role>Mera</role>
+ <order>12</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMjA4NDkyODA3M15BMl5BanBnXkFtZTgwMzUzMjYzNzM@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Joe Morton</name>
+ <role>Silas Stone</role>
+ <order>13</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BMTQ1MjYwMTQ2MF5BMl5BanBnXkFtZTgwNzI4MTA0NDE@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Lisa Loven Kongsli</name>
+ <role>Menalippe</role>
+ <order>14</order>
+ <thumb>https://m.media-amazon.com/images/M/MV5BOTFjOTFhNTgtZjk3Ny00MTNjLWE3MWUtMWI3ZWM5NDljZjQwXkEyXkFqcGdeQXVyMjQwMDg0Ng@@._V1_SX1024_SY1024_.jpg</thumb>
+ </actor>
+ <resume>
+ <position>0.000000</position>
+ <total>0.000000</total>
+ </resume>
+ <showlink>Justice League</showlink>
+ <dateadded>2019-08-06 09:01:18</dateadded>
+</movie>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Season 01.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Season 01.nfo
new file mode 100644
index 000000000..91f0392f4
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Season 01.nfo
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<season>
+ <plot />
+ <outline />
+ <lockdata>false</lockdata>
+ <dateadded>2020-06-14 17:26:51</dateadded>
+ <title>Season 1</title>
+ <year>2019</year>
+ <tvdbid>359728</tvdbid>
+ <premiered>2019-11-08</premiered>
+ <releasedate>2019-11-08</releasedate>
+ <art>
+ <poster>/media/Serien/High School Musical The Musical The Series (2019)/Season 1/Season 1.jpeg</poster>
+ </art>
+ <actor>
+ <name>Olivia Rodrigo</name>
+ <role>Nini</role>
+ <type>Actor</type>
+ <sortorder>0</sortorder>
+ <thumb>/config/metadata/People/O/Olivia Rodrigo/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Kate Reinders</name>
+ <role>Miss Jenn</role>
+ <type>Actor</type>
+ <sortorder>1</sortorder>
+ <thumb>/config/metadata/People/K/Kate Reinders/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Sofia Wylie</name>
+ <role>Gina</role>
+ <type>Actor</type>
+ <sortorder>2</sortorder>
+ <thumb>/config/metadata/People/S/Sofia Wylie/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Matt Cornett</name>
+ <role>E.J.</role>
+ <type>Actor</type>
+ <sortorder>3</sortorder>
+ <thumb>/config/metadata/People/M/Matt Cornett/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Dara Reneé</name>
+ <role>Kourtney</role>
+ <type>Actor</type>
+ <sortorder>4</sortorder>
+ <thumb>/config/metadata/People/D/Dara Reneé/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Julia Lester</name>
+ <role>Ashlyn</role>
+ <type>Actor</type>
+ <sortorder>5</sortorder>
+ <thumb>/config/metadata/People/J/Julia Lester/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Joshua Bassett</name>
+ <role>Ricky</role>
+ <type>Actor</type>
+ <sortorder>6</sortorder>
+ <thumb>/config/metadata/People/J/Joshua Bassett/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Frankie A. Rodriguez</name>
+ <role>Carlos</role>
+ <type>Actor</type>
+ <sortorder>7</sortorder>
+ <thumb>/config/metadata/People/F/Frankie A. Rodriguez/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Larry Saperstein</name>
+ <role>Big Red</role>
+ <type>Actor</type>
+ <sortorder>8</sortorder>
+ <thumb>/config/metadata/People/L/Larry Saperstein/poster.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Mark St. Cyr</name>
+ <role>Mr. Mazzara</role>
+ <type>Actor</type>
+ <sortorder>9</sortorder>
+ <thumb>/config/metadata/People/M/Mark St. Cyr/poster.jpg</thumb>
+ </actor>
+ <seasonnumber>1</seasonnumber>
+</season>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Best of 1980-1990.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Best of 1980-1990.nfo
new file mode 100644
index 000000000..4ab8400d3
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Best of 1980-1990.nfo
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<album>
+ <title>The Best of 1980-1990</title>
+ <musicbrainzalbumid>59b5a40b-e2fd-3f18-a218-e8c9aae12ab5</musicbrainzalbumid>
+ <musicbrainzreleasegroupid>6c301dbd-6ccb-3403-a6c4-6a22240a0297</musicbrainzreleasegroupid>
+ <scrapedmbid>false</scrapedmbid>
+ <artistdesc>U2</artistdesc>
+ <genre>Pop</genre>
+ <style>Rock/Pop</style>
+ <mood>Political</mood>
+ <compilation>false</compilation>
+ <review>The Best of 1980-1990 is the first greatest hits compilation by Irish rock band U2, released in November 1998. It mostly contains the group&apos;s hit singles from the eighties but also mixes in some live staples as well as one new recording, Sweetest Thing. In April 1999, a companion video (featuring music videos and live footage) was released. The album was followed by another compilation, The Best of 1990-2000, in 2002.&#x0A;A limited edition version containing a special B-sides disc was released on the same date as the single-disc version. At the time of release, the official word was that the 2-disc album would be available the first week the album went on sale, then pulled from the stores. While this threat never materialized, it did result in the 2-disc version being in very high demand. Both versions charted in the Billboard 200.&#x0A;The boy on the cover is Peter Rowan, brother of Bono&apos;s friend Guggi (real name Derek Rowan) of the Virgin Prunes. He also appears on the covers of the early EP Three, two of the band&apos;s first three albums (Boy and War), and Early Demos.</review>
+ <type>album / compilation</type>
+ <releasedate></releasedate>
+ <label>Island</label>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/albumcover/the-best-of-1980-1990-4e43a22cab023.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/albumcover/the-best-of-1980-1990-4e43a22cab023.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/albumcover/the-best-of-1980-1990-5bc4301068645.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/albumcover/the-best-of-1980-1990-5bc4301068645.jpg</thumb>
+ <thumb preview="https://www.theaudiodb.com/images/media/album/thumb/the-best-of-1980-1990-4e43a22cab023.jpg/preview">https://www.theaudiodb.com/images/media/album/thumb/the-best-of-1980-1990-4e43a22cab023.jpg</thumb>
+ <path>C:\KODI\Test- Music\U2\Best Of 1980-1990, The\</path>
+ <rating max="10">-1.000000</rating>
+ <userrating max="10">-1</userrating>
+ <votes>-1</votes>
+ <year>1989</year>
+ <albumArtistCredits>
+ <artist>U2</artist>
+ <musicBrainzArtistID>a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432</musicBrainzArtistID>
+ </albumArtistCredits>
+ <releasetype>album</releasetype>
+</album>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo
new file mode 100644
index 000000000..e77c02a34
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/The Bone Orchard.nfo
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<episodedetails>
+ <title>The Bone Orchard</title>
+ <showtitle>American Gods</showtitle>
+ <ratings>
+ <rating name="tmdb" max="10" default="true">
+ <value>7.532000</value>
+ <votes>31</votes>
+ </rating>
+ </ratings>
+ <userrating>0</userrating>
+ <top250>0</top250>
+ <season>1</season>
+ <episode>1</episode>
+ <displayseason>-1</displayseason>
+ <displayepisode>-1</displayepisode>
+ <outline></outline>
+ <plot>When Shadow Moon is released from prison early after the death of his wife, he meets Mr. Wednesday and is recruited as his bodyguard. Shadow discovers that this may be more than he bargained for.</plot>
+ <tagline></tagline>
+ <runtime>0</runtime>
+ <thumb>http://image.tmdb.org/t/p/original/uvry4weK00pFLn7fxQ9M4m3Da2A.jpg</thumb>
+ <mpaa>16</mpaa>
+ <playcount>0</playcount>
+ <lastplayed></lastplayed>
+ <id>1276153</id>
+ <uniqueid type="tmdb" default="true">1276153</uniqueid>
+ <genre>Drama</genre>
+ <genre>Mystery</genre>
+ <genre>Sci-Fi &amp; Fantasy</genre>
+ <credits>Bryan Fuller</credits>
+ <credits>Michael Green</credits>
+ <director>David Slade</director>
+ <premiered>2017-04-30</premiered>
+ <year>2017</year>
+ <status></status>
+ <code></code>
+ <aired>2017-04-30</aired>
+ <studio>Starz</studio>
+ <trailer></trailer>
+ <actor>
+ <name>Jonathan Tucker</name>
+ <role>&apos;Low Key&apos; Lyesmith</role>
+ <order>10</order>
+ <thumb>http://image.tmdb.org/t/p/original/jvJpYDbwmUTACw7Yn7PKOP6CdlJ.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Demore Barnes</name>
+ <role>Mr. Ibis</role>
+ <order>11</order>
+ <thumb>http://image.tmdb.org/t/p/original/4rEVzSIFPgiN14xYQnjKcKQ7tYE.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Betty Gilpin</name>
+ <role>Audrey</role>
+ <order>12</order>
+ <thumb>http://image.tmdb.org/t/p/original/xFeqyem5i4Kf0nFjBZ4Oi9NM26k.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Beth Grant</name>
+ <role>Jack</role>
+ <order>13</order>
+ <thumb>http://image.tmdb.org/t/p/original/zAT9GvzJE0ytL3C36L461cgKI9p.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Joel Murray</name>
+ <role>Paunch</role>
+ <order>14</order>
+ <thumb>http://image.tmdb.org/t/p/original/t5syYfCgxbTC7XPrNeXhhhQULUf.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Ricky Whittle</name>
+ <role>Shadow Moon</role>
+ <order>0</order>
+ <thumb>http://image.tmdb.org/t/p/original/cjeDbVfBp6Qvb3C74Dfy7BKDTQN.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Ian McShane</name>
+ <role>Mr. Wednesday</role>
+ <order>1</order>
+ <thumb>http://image.tmdb.org/t/p/original/pY9ud4BJwHekNiO4MMItPbgkdAy.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Emily Browning</name>
+ <role>Laura Moon</role>
+ <order>2</order>
+ <thumb>http://image.tmdb.org/t/p/original/fa1Kyj02wxwcdS6EHb2i27TNXvU.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Pablo Schreiber</name>
+ <role>Mad Sweeney</role>
+ <order>3</order>
+ <thumb>http://image.tmdb.org/t/p/original/uo8YljeePz3pbj7gvWXdB4gOOW4.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Bruce Langley</name>
+ <role>Technical Boy</role>
+ <order>4</order>
+ <thumb>http://image.tmdb.org/t/p/original/f4EOWUmznLqboq8Ce7jnlkHVK3Y.jpg</thumb>
+ </actor>
+ <actor>
+ <name>Yetide Badaki</name>
+ <role>Bilquis</role>
+ <order>5</order>
+ <thumb>http://image.tmdb.org/t/p/original/qfzkREHuI1JvMxBteIAjKX8qMEr.jpg</thumb>
+ </actor>
+ <resume>
+ <position>0.000000</position>
+ <total>0.000000</total>
+ </resume>
+ <dateadded>2017-10-07 14:25:47</dateadded>
+</episodedetails>
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/U2.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/U2.nfo
new file mode 100644
index 000000000..8c46fdeb8
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/U2.nfo
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<artist>
+ <name>U2</name>
+ <musicBrainzArtistID>a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432</musicBrainzArtistID>
+ <sortname>U2</sortname>
+ <type></type>
+ <gender></gender>
+ <disambiguation>Irish rock band</disambiguation>
+ <genre>Rock</genre>
+ <style>Rock/Pop</style>
+ <mood>Political</mood>
+ <born></born>
+ <formed>Dublin, Ireland (1976)</formed>
+ <biography>U2 are an Irish rock band from Dublin. Formed in 1976, the group consists of Bono (vocals and rhythm guitar), the Edge (lead guitar, keyboards, and vocals), Adam Clayton (bass guitar), and Larry Mullen, Jr. (drums and percussion). U2&apos;s early sound was rooted in post-punk but eventually grew to incorporate influences from many genres of popular music. Throughout the group&apos;s musical pursuits, they have maintained a sound built on melodic instrumentals. Their lyrics, often embellished with spiritual imagery, focus on personal themes and sociopolitical concerns.&#x0A;The band formed at Mount Temple Comprehensive School in 1976 when the members were teenagers with limited musical proficiency. Within four years, they signed with Island Records and released their debut album Boy. By the mid-1980s, U2 had become a top international act. They were more successful as a touring act than they were at selling records until their 1987 album The Joshua Tree which, according to Rolling Stone, elevated the band&apos;s stature &quot;from heroes to superstars&quot;. Reacting to musical stagnation and criticism of their earnest image and musical direction in the late 1980s, U2 reinvented themselves with their 1991 album, Achtung Baby, and the accompanying Zoo TV Tour; they integrated dance, industrial, and alternative rock influences into their sound, and embraced a more ironic and self-deprecating image. They embraced similar experimentation for the remainder of the 1990s with varying levels of success. U2 regained critical and commercial favour in the 2000s with the records All That You Can&apos;t Leave Behind (2000) and How to Dismantle an Atomic Bomb (2004), which established a more conventional, mainstream sound for the group. Their U2 360° Tour of 2009–2011 is the highest-attended and highest-grossing concert tour in history.&#x0A;U2 have released 13 studio albums and are one of the world&apos;s best-selling music artists of all time, having sold more than 170 million records worldwide. They have won 22 Grammy Awards, more than any other band; and, in 2005, were inducted into the Rock and Roll Hall of Fame in their first year of eligibility. Rolling Stone ranked U2 at number 22 in its list of the &quot;100 Greatest Artists of All Time&quot;, and labelled them the &quot;Biggest Band in the World&quot;. Throughout their career, as a band and as individuals, they have campaigned for human rights and philanthropic causes, including Amnesty International, the ONE/DATA campaigns, Product Red, War Child and the Edge&apos;s Music Rising.</biography>
+ <died></died>
+ <disbanded></disbanded>
+ <thumb aspect="poster" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistthumb/u2-50104c356fd2b.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistthumb/u2-50104c356fd2b.jpg</thumb>
+ <thumb aspect="poster" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistthumb/u2-4fdf7ab5bfa99.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistthumb/u2-4fdf7ab5bfa99.jpg</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-538b320283c98.png">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-538b320283c98.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-538b3268cc581.png">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-538b3268cc581.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-55b02a97170c7.png">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-55b02a97170c7.png</thumb>
+ <thumb aspect="clearlogo" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-538b39954caf1.png">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/hdmusiclogo/u2-538b39954caf1.png</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-59e65cba172de.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-59e65cba172de.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-54063da8ca135.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-54063da8ca135.jpg</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-503f9e062c802.JPG">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-503f9e062c802.JPG</thumb>
+ <thumb aspect="banner" preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-591ce819c91a5.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/musicbanner/u2-591ce819c91a5.jpg</thumb>
+ <thumb preview="https://www.theaudiodb.com/images/media/artist/thumb/qvuxvs1347997318.jpg/preview">https://www.theaudiodb.com/images/media/artist/thumb/qvuxvs1347997318.jpg</thumb>
+ <thumb aspect="clearlogo" preview="https://www.theaudiodb.com/images/media/artist/logo/qywsvv1347997327.png/preview">https://www.theaudiodb.com/images/media/artist/logo/qywsvv1347997327.png</thumb>
+ <thumb aspect="clearart" preview="https://www.theaudiodb.com/images/media/artist/clearart/vwpyxv1511531849.png/preview">https://www.theaudiodb.com/images/media/artist/clearart/vwpyxv1511531849.png</thumb>
+ <thumb aspect="landscape" preview="https://www.theaudiodb.com/images/media/artist/widethumb/wxsxwq1524669620.jpg/preview">https://www.theaudiodb.com/images/media/artist/widethumb/wxsxwq1524669620.jpg</thumb>
+ <thumb aspect="banner" preview="https://www.theaudiodb.com/images/media/artist/banner/rpqwpu1488384726.jpg/preview">https://www.theaudiodb.com/images/media/artist/banner/rpqwpu1488384726.jpg</thumb>
+ <path>E:\z-Music Artists\U2</path>
+ <fanart>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4f805e377b181.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4f805e377b181.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-5058bffb80200.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-5058bffb80200.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4f805e377b5a0.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4f805e377b5a0.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4df96804ad0f3.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4df96804ad0f3.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-5487022bd1524.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-5487022bd1524.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-50104bf699b84.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-50104bf699b84.jpg</thumb>
+ <thumb preview="https://assets.fanart.tv/preview/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4f805e377acdb.jpg">https://assets.fanart.tv/fanart/music/a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432/artistbackground/u2-4f805e377acdb.jpg</thumb>
+ </fanart>
+ <album>
+ <title>Pop</title>
+ <year>1997</year>
+ </album>
+ <album>
+ <title>How to Dismantle an Atomic Bomb</title>
+ <year>2004</year>
+ </album>
+ <album>
+ <title>Boy</title>
+ <year>1980</year>
+ </album>
+ <album>
+ <title>Pop</title>
+ <year>1997</year>
+ </album>
+ <album>
+ <title>The Joshua Tree</title>
+ <year>1987</year>
+ </album>
+ <album>
+ <title>Achtung Baby</title>
+ <year>1991</year>
+ </album>
+ <album>
+ <title>Zooropa</title>
+ <year>1993</year>
+ </album>
+</artist>