aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/Main/DlnaEntryPoint.cs2
-rw-r--r--Emby.Notifications/CoreNotificationTypes.cs8
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs20
-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/ca.json19
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json28
-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-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/Triggers/DailyTrigger.cs2
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs1
-rw-r--r--Jellyfin.Networking/Manager/NetworkManager.cs4
-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/Notifications/NotificationType.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs14
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs184
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs2
-rw-r--r--MediaBrowser.sln9
-rw-r--r--fedora/jellyfin.spec2
-rw-r--r--jellyfin.ruleset2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj2
-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.csproj2
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs12
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj2
-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/MovieNfoParserTests.cs107
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs70
-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/U2.nfo70
52 files changed, 1312 insertions, 147 deletions
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/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.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/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/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..4a505d0b3 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -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/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-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/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/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.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/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/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.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/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.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/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..07972bb42 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -21,7 +21,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.0" />
<PackageReference Include="Moq" Version="4.15.2" />
</ItemGroup>
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 19c5612c0..fdeeda5a3 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.0" />
</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..84655db24 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.0" />
</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..c5b01f4db 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.0" />
</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..ed788bab8 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.0" />
</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..f3b00dcab 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.0" />
</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..8d9c20de1 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.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.1" />
- <PackageReference Include="coverlet.collector" Version="1.3.0" />
+ <PackageReference Include="coverlet.collector" Version="3.0.0" />
<PackageReference Include="Moq" Version="4.15.2" />
</ItemGroup>
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..5c4170514 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -20,7 +20,7 @@
<PackageReference Include="Moq" Version="4.15.2" />
<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.0" />
</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..f02ac03f7
--- /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.15.2" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+ <PackageReference Include="coverlet.collector" Version="3.0.0" />
+ </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/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
new file mode 100644
index 000000000..7651653a1
--- /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.Parsers.Tests
+{
+ 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/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs
new file mode 100644
index 000000000..a8c6e5afd
--- /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.Parsers.Tests
+{
+ 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/SeriesNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/SeriesNfoParserTests.cs
new file mode 100644
index 000000000..37ca0fd05
--- /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.Parsers.Tests
+{
+ 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/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>