diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 31ae502634..85590c0b0a 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -86,7 +86,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: - - 10.9.7 + - 10.9.8+ - Master - Unstable - Older* diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index c6ea1d7ca9..ba66526e00 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 + uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 + uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 + uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 54a0615567..d5dc1a8602 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -27,7 +27,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 with: name: openapi-head retention-days: 14 @@ -61,7 +61,7 @@ jobs: - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5 with: name: openapi-base retention-days: 14 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index edbc846d63..7c2e72327a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -185,6 +185,7 @@ - [Vedant](https://github.com/viktory36/) - [NotSaifA](https://github.com/NotSaifA) - [HonestlyWhoKnows](https://github.com/honestlywhoknows) + - [TheMelmacian](https://github.com/TheMelmacian) - [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode) # Emby Contributors diff --git a/Directory.Packages.props b/Directory.Packages.props index 825301bfcb..683f2eee32 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -22,7 +22,7 @@ - + @@ -58,7 +58,7 @@ - + @@ -72,7 +72,7 @@ - + @@ -81,6 +81,7 @@ + diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index fc9ee8e569..45b91971bf 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -24,6 +24,8 @@ namespace Emby.Naming.TV "stagione" }; + private static readonly char[] _splitChars = ['.', '_', ' ', '-']; + /// /// Attempts to parse season number from path. /// @@ -83,14 +85,9 @@ namespace Emby.Naming.TV } } - if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase)) + if (TryGetSeasonNumberFromPart(filename, out int seasonNumber)) { - var testFilename = filename.AsSpan().Slice(1); - - if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } + return (seasonNumber, true); } // Look for one of the season folder names @@ -108,10 +105,10 @@ namespace Emby.Naming.TV } } - var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); + var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { - if (TryGetSeasonNumberFromPart(part, out int seasonNumber)) + if (TryGetSeasonNumberFromPart(part, out seasonNumber)) { return (seasonNumber, true); } diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index e2f1ca813c..ac6c41ca52 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -26,7 +26,7 @@ public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IH private readonly ILogger _logger; private readonly IImageProcessor _imageProcessor; - // These are causing taglib to hang + // Other extensions might cause taglib to hang private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"]; /// diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 634eaf85ef..bfdcc08f42 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -333,10 +333,10 @@ namespace Emby.Server.Implementations.Data /// The user item data. private UserItemData ReadRow(SqliteDataReader reader) { - var userData = new UserItemData(); - - userData.Key = reader[0].ToString(); - // userData.UserId = reader[1].ReadGuidFromBlob(); + var userData = new UserItemData + { + Key = reader.GetString(0) + }; if (reader.TryGetDouble(2, out var rating)) { diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 19902b26a0..0c0ba74533 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -83,12 +81,12 @@ namespace Emby.Server.Implementations.Dto private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; /// - public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null) + public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null) { var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList(); var returnItems = new BaseItemDto[accessibleItems.Count]; - List<(BaseItem, BaseItemDto)> programTuples = null; - List<(BaseItemDto, LiveTvChannel)> channelTuples = null; + List<(BaseItem, BaseItemDto)>? programTuples = null; + List<(BaseItemDto, LiveTvChannel)>? channelTuples = null; for (int index = 0; index < accessibleItems.Count; index++) { @@ -137,7 +135,7 @@ namespace Emby.Server.Implementations.Dto return returnItems; } - public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) + public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null) { var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) @@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Dto return dto; } - private static IList GetTaggedItems(IItemByName byName, User user, DtoOptions options) + private static IList GetTaggedItems(IItemByName byName, User? user, DtoOptions options) { return byName.GetTaggedItems( new InternalItemsQuery(user) @@ -177,7 +175,7 @@ namespace Emby.Server.Implementations.Dto }); } - private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) + private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null) { var dto = new BaseItemDto { @@ -292,7 +290,7 @@ namespace Emby.Server.Implementations.Dto } var path = mediaSource.Path; - string fileExtensionContainer = null; + string? fileExtensionContainer = null; if (!string.IsNullOrEmpty(path)) { @@ -316,7 +314,8 @@ namespace Emby.Server.Implementations.Dto } } - public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null) + /// + public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List? taggedItems, User? user = null) { var dto = GetBaseItemDtoInternal(item, options, user); @@ -486,10 +485,10 @@ namespace Emby.Server.Implementations.Dto return images .Select(p => GetImageCacheTag(item, p)) .Where(i => i is not null) - .ToArray(); + .ToArray()!; // null values got filtered out } - private string GetImageCacheTag(BaseItem item, ItemImageInfo image) + private string? GetImageCacheTag(BaseItem item, ItemImageInfo image) { try { @@ -508,7 +507,7 @@ namespace Emby.Server.Implementations.Dto /// The dto. /// The item. /// The requesting user. - private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null) + private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null) { // Ordering by person type to ensure actors and artists are at the front. // This is taking advantage of the fact that they both begin with A @@ -552,7 +551,7 @@ namespace Emby.Server.Implementations.Dto var list = new List(); - var dictionary = people.Select(p => p.Name) + Dictionary dictionary = people.Select(p => p.Name) .Distinct(StringComparer.OrdinalIgnoreCase).Select(c => { try @@ -565,9 +564,9 @@ namespace Emby.Server.Implementations.Dto return null; } }).Where(i => i is not null) - .Where(i => user is null || i.IsVisible(user)) - .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase) - .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + .Where(i => user is null || i!.IsVisible(user)) + .DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out for (var i = 0; i < people.Count; i++) { @@ -580,7 +579,7 @@ namespace Emby.Server.Implementations.Dto Type = person.Type }; - if (dictionary.TryGetValue(person.Name, out Person entity)) + if (dictionary.TryGetValue(person.Name, out Person? entity)) { baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); baseItemPerson.Id = entity.Id; @@ -650,7 +649,7 @@ namespace Emby.Server.Implementations.Dto return _libraryManager.GetGenreId(name); } - private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0) + private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0) { var image = item.GetImageInfo(imageType, imageIndex); if (image is not null) @@ -661,9 +660,14 @@ namespace Emby.Server.Implementations.Dto return null; } - private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image) + private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image) { var tag = GetImageCacheTag(item, image); + if (tag is null) + { + return null; + } + if (!string.IsNullOrEmpty(image.BlurHash)) { dto.ImageBlurHashes ??= new Dictionary>(); @@ -716,7 +720,7 @@ namespace Emby.Server.Implementations.Dto /// The item. /// The owner. /// The options. - private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options) + private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options) { if (options.ContainsField(ItemFields.DateCreated)) { @@ -1097,7 +1101,7 @@ namespace Emby.Server.Implementations.Dto } } - BaseItem[] allExtras = null; + BaseItem[]? allExtras = null; if (options.ContainsField(ItemFields.SpecialFeatureCount)) { @@ -1134,7 +1138,7 @@ namespace Emby.Server.Implementations.Dto dto.SeasonId = episode.SeasonId; dto.SeriesId = episode.SeriesId; - Series episodeSeries = null; + Series? episodeSeries = null; // this block will add the series poster for episodes without a poster // TODO maybe remove the if statement entirely @@ -1162,8 +1166,10 @@ namespace Emby.Server.Implementations.Dto } // Add SeriesInfo - if (item is Series series) + Series? series; + if (item is Series tmp) { + series = tmp; dto.AirDays = series.AirDays; dto.AirTime = series.AirTime; dto.Status = series.Status?.ToString(); @@ -1264,7 +1270,7 @@ namespace Emby.Server.Implementations.Dto } } - private BaseItem GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem) + private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem) { if (currentItem is MusicAlbum musicAlbum) { @@ -1285,7 +1291,7 @@ namespace Emby.Server.Implementations.Dto return parent; } - private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner) + private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner) { if (!item.SupportsInheritedParentImages) { @@ -1305,7 +1311,7 @@ namespace Emby.Server.Implementations.Dto return; } - BaseItem parent = null; + BaseItem? parent = null; var isFirst = true; var imageTags = dto.ImageTags; @@ -1378,7 +1384,7 @@ namespace Emby.Server.Implementations.Dto } } - private string GetMappedPath(BaseItem item, BaseItem ownerItem) + private string GetMappedPath(BaseItem item, BaseItem? ownerItem) { var path = item.Path; diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 957ad9c01b..47f9dfbc87 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -138,13 +137,13 @@ namespace Emby.Server.Implementations.EntryPoints return new UserDataChangeInfo { - UserId = userId.ToString("N", CultureInfo.InvariantCulture), + UserId = userId, UserDataList = changedItems .DistinctBy(x => x.Id) .Select(i => { var dto = _userDataManager.GetUserDataDto(i, user); - dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture); + dto.ItemId = i.Id; return dto; }) .ToArray() diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index bb22ca82fa..90a01c052c 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -379,7 +379,8 @@ namespace Emby.Server.Implementations.Library private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { - if (userData.SubtitleStreamIndex.HasValue + if (userData is not null + && userData.SubtitleStreamIndex.HasValue && user.RememberSubtitleSelections && user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) @@ -411,7 +412,7 @@ namespace Emby.Server.Implementations.Library private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { - if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection) + if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection) { var index = userData.AudioStreamIndex.Value; // Make sure the saved index is still valid @@ -434,7 +435,7 @@ namespace Emby.Server.Implementations.Library if (mediaType == MediaType.Video) { - var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item); + var userData = item is null ? null : _userDataManager.GetUserData(user, item); var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs index 955055313e..4b15073858 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs @@ -68,11 +68,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var justName = Path.GetFileName(item.Path.AsSpan()); var id = justName.GetAttributeValue("tmdbid"); - - if (!string.IsNullOrEmpty(id)) - { - item.SetProviderId(MetadataProvider.Tmdb, id); - } + item.TrySetProviderId(MetadataProvider.Tmdb, id); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 1a210e3cc8..4debe722b9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -373,22 +373,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies { // Check for TMDb id var tmdbid = justName.GetAttributeValue("tmdbid"); - - if (!string.IsNullOrWhiteSpace(tmdbid)) - { - item.SetProviderId(MetadataProvider.Tmdb, tmdbid); - } + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid); } if (!string.IsNullOrEmpty(item.Path)) { // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid"); - - if (!string.IsNullOrWhiteSpace(imdbid)) - { - item.SetProviderId(MetadataProvider.Imdb, imdbid); - } + item.TrySetProviderId(MetadataProvider.Imdb, imdbid); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 1484c34bcc..fb48d7bf17 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -186,46 +186,25 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var justName = Path.GetFileName(path.AsSpan()); var imdbId = justName.GetAttributeValue("imdbid"); - if (!string.IsNullOrEmpty(imdbId)) - { - item.SetProviderId(MetadataProvider.Imdb, imdbId); - } + item.TrySetProviderId(MetadataProvider.Imdb, imdbId); var tvdbId = justName.GetAttributeValue("tvdbid"); - if (!string.IsNullOrEmpty(tvdbId)) - { - item.SetProviderId(MetadataProvider.Tvdb, tvdbId); - } + item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId); var tvmazeId = justName.GetAttributeValue("tvmazeid"); - if (!string.IsNullOrEmpty(tvmazeId)) - { - item.SetProviderId(MetadataProvider.TvMaze, tvmazeId); - } + item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId); var tmdbId = justName.GetAttributeValue("tmdbid"); - if (!string.IsNullOrEmpty(tmdbId)) - { - item.SetProviderId(MetadataProvider.Tmdb, tmdbId); - } + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); var anidbId = justName.GetAttributeValue("anidbid"); - if (!string.IsNullOrEmpty(anidbId)) - { - item.SetProviderId("AniDB", anidbId); - } + item.TrySetProviderId("AniDB", anidbId); var aniListId = justName.GetAttributeValue("anilistid"); - if (!string.IsNullOrEmpty(aniListId)) - { - item.SetProviderId("AniList", aniListId); - } + item.TrySetProviderId("AniList", aniListId); var aniSearchId = justName.GetAttributeValue("anisearchid"); - if (!string.IsNullOrEmpty(aniSearchId)) - { - item.SetProviderId("AniSearch", aniSearchId); - } + item.TrySetProviderId("AniSearch", aniSearchId); } } } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 8bd3c5defe..9433da28b1 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -124,5 +124,11 @@ "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "HearingImpaired": "Discapacidad Auditiva" + "HearingImpaired": "Discapacidad Auditiva", + "TaskRefreshTrickplayImages": "Generar imágenes de Trickplay", + "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.", + "TaskAudioNormalization": "Normalización de audio", + "TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.", + "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción", + "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen." } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 55ee1abaab..28c1d2be5e 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -69,7 +69,7 @@ "HeaderLiveTV": "Live TV", "HeaderFavoriteSongs": "Mga Paboritong Kanta", "HeaderFavoriteShows": "Mga Paboritong Pelikula", - "HeaderFavoriteEpisodes": "Mga Paboritong Episode", + "HeaderFavoriteEpisodes": "Mga Paboritong Yugto", "HeaderFavoriteArtists": "Mga Paboritong Artista", "HeaderFavoriteAlbums": "Mga Paboritong Album", "HeaderContinueWatching": "Magpatuloy sa Panonood", diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 6a5b8c5615..a7dabaa19f 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -11,7 +11,7 @@ "Collections": "Kolekcije", "DeviceOfflineWithName": "{0} je prekinuo vezu", "DeviceOnlineWithName": "{0} je povezan", - "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}", + "FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}", "Favorites": "Favoriti", "Folders": "Mape", "Genres": "Žanrovi", @@ -127,5 +127,8 @@ "HearingImpaired": "Oštećen sluh", "TaskRefreshTrickplayImages": "Generiraj Trickplay Slike", "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.", - "TaskAudioNormalization": "Normalizacija zvuka" + "TaskAudioNormalization": "Normalizacija zvuka", + "TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.", + "TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.", + "TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju" } diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index b91889594b..a739cba358 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -125,5 +125,10 @@ "TaskKeyframeExtractor": "키프레임 추출", "External": "외부", "HearingImpaired": "청각 장애", - "TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리" + "TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리", + "TaskAudioNormalization": "오디오의 볼륨 수준을 일정하게 조정", + "TaskAudioNormalizationDescription": "오디오의 볼륨 수준을 일정하게 조정하기 위해 파일을 스캔합니다.", + "TaskRefreshTrickplayImages": "비디오 탐색용 미리보기 썸네일 생성", + "TaskRefreshTrickplayImagesDescription": "활성화된 라이브러리에서 비디오의 트릭플레이 미리보기를 생성합니다.", + "TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다." } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 78c3d0a409..62277fd94a 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -76,7 +76,7 @@ "Genres": "Žanri", "Folders": "Mapes", "Favorites": "Izlase", - "FailedLoginAttemptWithUserName": "Neveiksmīgs ielogošanos mēģinājums no {0}", + "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}", "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots", "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts", "Collections": "Kolekcijas", @@ -95,7 +95,7 @@ "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus", "TasksApplicationCategory": "Lietotne", "TasksLibraryCategory": "Bibliotēka", - "TaskDownloadMissingSubtitlesDescription": "Meklē trūkstošus subtitrus internēta balstoties uz metadatu uzstādījumiem.", + "TaskDownloadMissingSubtitlesDescription": "Meklē internetā trūkstošos subtitrus, pamatojoties uz metadatu konfigurāciju.", "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus", "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.", "TaskRefreshChannels": "Atjaunot kanālus", @@ -127,7 +127,7 @@ "TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus", "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.", "TaskAudioNormalization": "Audio normalizācija", - "TaskCleanCollectionsAndPlaylistsDescription": "Noņem elemēntus no kolekcijām un atskaņošanas sarakstiem, kuri vairs neeksistē.", + "TaskCleanCollectionsAndPlaylistsDescription": "Noņem vairs neeksistējošus vienumus no kolekcijām un atskaņošanas sarakstiem.", "TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.", "TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus" } diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 329dd2c4cb..130c1192f7 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1733,7 +1733,7 @@ public class DynamicHlsController : BaseJellyfinApiController var channels = state.OutputAudioChannels; - var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None; + var useDownMixAlgorithm = DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((_encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream))); if (channels.HasValue && (channels.Value != 2 diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 2b26c01f88..6c3d011036 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -656,7 +656,7 @@ public class LiveTvController : BaseJellyfinApiController var query = new InternalItemsQuery(user) { - ChannelIds = body.ChannelIds, + ChannelIds = body.ChannelIds ?? [], HasAired = body.HasAired, IsAiring = body.IsAiring, EnableTotalRecordCount = body.EnableTotalRecordCount, @@ -666,31 +666,31 @@ public class LiveTvController : BaseJellyfinApiController MaxEndDate = body.MaxEndDate, StartIndex = body.StartIndex, Limit = body.Limit, - OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder), + OrderBy = RequestHelpers.GetOrderBy(body.SortBy ?? [], body.SortOrder ?? []), IsNews = body.IsNews, IsMovie = body.IsMovie, IsSeries = body.IsSeries, IsKids = body.IsKids, IsSports = body.IsSports, SeriesTimerId = body.SeriesTimerId, - Genres = body.Genres, - GenreIds = body.GenreIds + Genres = body.Genres ?? [], + GenreIds = body.GenreIds ?? [] }; - if (!body.LibrarySeriesId.IsEmpty()) + if (!body.LibrarySeriesId.IsNullOrEmpty()) { query.IsSeries = true; - var series = _libraryManager.GetItemById(body.LibrarySeriesId); + var series = _libraryManager.GetItemById(body.LibrarySeriesId.Value); if (series is not null) { query.Name = series.Name; } } - var dtoOptions = new DtoOptions { Fields = body.Fields } + var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] } .AddClientFields(User) - .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); + .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 3d17dbda18..f52b58babf 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -26,8 +26,6 @@ public static class DtoExtensions internal static DtoOptions AddClientFields( this DtoOptions dtoOptions, ClaimsPrincipal user) { - dtoOptions.Fields ??= Array.Empty(); - string? client = user.GetClient(); // No client in claim diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index cb178a61d8..0690f0c8d7 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -38,7 +38,7 @@ public static class FileStreamResponseHelpers } // Can't dispose the response as it's required up the call chain. - var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false); + var response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain; httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none"; diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 8482b1cf1c..7210cc8f7e 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json.Converters; @@ -17,7 +18,7 @@ public class GetProgramsDto /// Gets or sets the channels to return guide information for. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList ChannelIds { get; set; } = Array.Empty(); + public IReadOnlyList? ChannelIds { get; set; } /// /// Gets or sets optional. Filter by user id. @@ -26,153 +27,133 @@ public class GetProgramsDto /// /// Gets or sets the minimum premiere start date. - /// Optional. /// public DateTime? MinStartDate { get; set; } /// /// Gets or sets filter by programs that have completed airing, or not. - /// Optional. /// public bool? HasAired { get; set; } /// /// Gets or sets filter by programs that are currently airing, or not. - /// Optional. /// public bool? IsAiring { get; set; } /// /// Gets or sets the maximum premiere start date. - /// Optional. /// public DateTime? MaxStartDate { get; set; } /// /// Gets or sets the minimum premiere end date. - /// Optional. /// public DateTime? MinEndDate { get; set; } /// /// Gets or sets the maximum premiere end date. - /// Optional. /// public DateTime? MaxEndDate { get; set; } /// /// Gets or sets filter for movies. - /// Optional. /// public bool? IsMovie { get; set; } /// /// Gets or sets filter for series. - /// Optional. /// public bool? IsSeries { get; set; } /// /// Gets or sets filter for news. - /// Optional. /// public bool? IsNews { get; set; } /// /// Gets or sets filter for kids. - /// Optional. /// public bool? IsKids { get; set; } /// /// Gets or sets filter for sports. - /// Optional. /// public bool? IsSports { get; set; } /// /// Gets or sets the record index to start at. All items with a lower index will be dropped from the results. - /// Optional. /// public int? StartIndex { get; set; } /// /// Gets or sets the maximum number of records to return. - /// Optional. /// public int? Limit { get; set; } /// /// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate. - /// Optional. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList SortBy { get; set; } = Array.Empty(); + public IReadOnlyList? SortBy { get; set; } /// - /// Gets or sets sort Order - Ascending,Descending. + /// Gets or sets sort order. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList SortOrder { get; set; } = Array.Empty(); + public IReadOnlyList? SortOrder { get; set; } /// /// Gets or sets the genres to return guide information for. /// [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))] - public IReadOnlyList Genres { get; set; } = Array.Empty(); + public IReadOnlyList? Genres { get; set; } /// /// Gets or sets the genre ids to return guide information for. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList GenreIds { get; set; } = Array.Empty(); + public IReadOnlyList? GenreIds { get; set; } /// /// Gets or sets include image information in output. - /// Optional. /// public bool? EnableImages { get; set; } /// /// Gets or sets a value indicating whether retrieve total record count. /// + [DefaultValue(true)] public bool EnableTotalRecordCount { get; set; } = true; /// /// Gets or sets the max number of images to return, per image type. - /// Optional. /// public int? ImageTypeLimit { get; set; } /// /// Gets or sets the image types to include in the output. - /// Optional. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList EnableImageTypes { get; set; } = Array.Empty(); + public IReadOnlyList? EnableImageTypes { get; set; } /// /// Gets or sets include user data. - /// Optional. /// public bool? EnableUserData { get; set; } /// /// Gets or sets filter by series timer id. - /// Optional. /// public string? SeriesTimerId { get; set; } /// /// Gets or sets filter by library series id. - /// Optional. /// - public Guid LibrarySeriesId { get; set; } + public Guid? LibrarySeriesId { get; set; } /// - /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. - /// Optional. + /// Gets or sets specify additional fields of information to return in the output. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - public IReadOnlyList Fields { get; set; } = Array.Empty(); + public IReadOnlyList? Fields { get; set; } } diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index ecc833154d..cb638cf90b 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index ecca440f05..15bd41a9c3 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -19,17 +17,11 @@ namespace MediaBrowser.Controller.Entities /// private double? _rating; - /// - /// Gets or sets the user id. - /// - /// The user id. - public Guid UserId { get; set; } - /// /// Gets or sets the key. /// /// The key. - public string Key { get; set; } + public required string Key { get; set; } /// /// Gets or sets the users 0-10 rating. diff --git a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs new file mode 100644 index 0000000000..749f872710 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// +/// Describes the downmix algorithms capabilities. +/// +public static class DownMixAlgorithmsHelper +{ + /// + /// The filter string of the DownMixStereoAlgorithms. + /// The index is the tuple of (algorithm, layout). + /// + public static readonly Dictionary<(DownMixStereoAlgorithms, string), string> AlgorithmFilterStrings = new() + { + { (DownMixStereoAlgorithms.Dave750, "5.1"), "pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" }, + // Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first + { (DownMixStereoAlgorithms.Dave750, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" }, + { (DownMixStereoAlgorithms.NightmodeDialogue, "5.1"), "pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" }, + // Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first + { (DownMixStereoAlgorithms.NightmodeDialogue, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" }, + { (DownMixStereoAlgorithms.Rfc7845, "3.0"), "pan=stereo|c0=0.414214*c2+0.585786*c0|c1=0.414214*c2+0.585786*c1" }, + { (DownMixStereoAlgorithms.Rfc7845, "quad"), "pan=stereo|c0=0.422650*c0+0.366025*c2+0.211325*c3|c1=0.422650*c1+0.366025*c3+0.211325*c2" }, + { (DownMixStereoAlgorithms.Rfc7845, "5.0"), "pan=stereo|c0=0.460186*c2+0.650802*c0+0.563611*c3+0.325401*c4|c1=0.460186*c2+0.650802*c1+0.563611*c4+0.325401*c3" }, + { (DownMixStereoAlgorithms.Rfc7845, "5.1"), "pan=stereo|c0=0.374107*c2+0.529067*c0+0.458186*c4+0.264534*c5+0.374107*c3|c1=0.374107*c2+0.529067*c1+0.458186*c5+0.264534*c4+0.374107*c3" }, + { (DownMixStereoAlgorithms.Rfc7845, "6.1"), "pan=stereo|c0=0.321953*c2+0.455310*c0+0.394310*c5+0.227655*c6+0.278819*c4+0.321953*c3|c1=0.321953*c2+0.455310*c1+0.394310*c6+0.227655*c5+0.278819*c4+0.321953*c3" }, + { (DownMixStereoAlgorithms.Rfc7845, "7.1"), "pan=stereo|c0=0.274804*c2+0.388631*c0+0.336565*c6+0.194316*c7+0.336565*c4+0.194316*c5+0.274804*c3|c1=0.274804*c2+0.388631*c1+0.336565*c7+0.194316*c6+0.336565*c5+0.194316*c4+0.274804*c3" }, + { (DownMixStereoAlgorithms.Ac4, "3.0"), "pan=stereo|c0=c0+0.707*c2|c1=c1+0.707*c2" }, + { (DownMixStereoAlgorithms.Ac4, "5.0"), "pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" }, + { (DownMixStereoAlgorithms.Ac4, "5.1"), "pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" }, + { (DownMixStereoAlgorithms.Ac4, "7.0"), "pan=5.0(side)|c0=c0|c1=c1|c2=c2|c3=0.707*c3+0.707*c5|c4=0.707*c4+0.707*c6,pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" }, + { (DownMixStereoAlgorithms.Ac4, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" }, + }; + + /// + /// Get the audio channel layout string from the audio stream + /// If the input audio string does not have a valid layout string, guess from channel count. + /// + /// The audio stream to get layout. + /// Channel Layout string. + public static string InferChannelLayout(MediaStream audioStream) + { + if (!string.IsNullOrWhiteSpace(audioStream.ChannelLayout)) + { + // Note: BDMVs do not derive this string from ffmpeg, which would cause ambiguity with 4-channel audio + // "quad" => 2 front and 2 rear, "4.0" => 3 front and 1 rear + // BDMV will always use "4.0" in this case + // Because the quad layout is super rare in BDs, we will use "4.0" as is here + return audioStream.ChannelLayout; + } + + if (audioStream.Channels is null) + { + return string.Empty; + } + + // When we don't have definitive channel layout, we have to guess from the channel count + // Guessing is not always correct, but for most videos we don't have to guess like this as the definitive layout is recorded during scan + var inferredLayout = audioStream.Channels.Value switch + { + 1 => "mono", + 2 => "stereo", + 3 => "2.1", // Could also be 3.0, prefer 2.1 + 4 => "4.0", // Could also be quad (with rear left and rear right) and 3.1 with LFE. prefer 4.0 with front center and back center + 5 => "5.0", + 6 => "5.1", // Could also be 6.0 or hexagonal, prefer 5.1 + 7 => "6.1", // Could also be 7.0, prefer 6.1 + 8 => "7.1", // Could also be 8.0, prefer 7.1 + _ => string.Empty // Return empty string for not supported layout + }; + return inferredLayout; + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index eb80bab2da..c068cf0559 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -64,6 +64,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0); private readonly Version _minFFmpegReadrateOption = new Version(5, 0); + private readonly Version _minFFmpegWorkingVtHwSurface = new Version(7, 0, 1); private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -2673,28 +2674,17 @@ namespace MediaBrowser.Controller.MediaEncoding var filters = new List(); - if (channels.HasValue - && channels.Value == 2 - && state.AudioStream is not null - && state.AudioStream.Channels.HasValue - && state.AudioStream.Channels.Value == 6) + if (channels is 2 && state.AudioStream?.Channels is > 2) { - if (!encodingOptions.DownMixAudioBoost.Equals(1)) + var hasDownMixFilter = DownMixAlgorithmsHelper.AlgorithmFilterStrings.TryGetValue((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)), out var downMixFilterString); + if (hasDownMixFilter) { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); + filters.Add(downMixFilterString); } - switch (encodingOptions.DownMixStereoAlgorithm) + if (!encodingOptions.DownMixAudioBoost.Equals(1)) { - case DownMixStereoAlgorithms.Dave750: - filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3"); - break; - case DownMixStereoAlgorithms.NightmodeDialogue: - filters.Add("pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5"); - break; - case DownMixStereoAlgorithms.None: - default: - break; + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); } } @@ -5300,6 +5290,7 @@ namespace MediaBrowser.Controller.MediaEncoding string vidEncoder) { var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); if (!isVtEncoder) { @@ -5320,6 +5311,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options); var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options); + var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface); var rotation = state.VideoStream?.Rotation ?? 0; var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); @@ -5414,23 +5406,25 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add(subTextSubtitlesFilter); } - subFilters.Add("hwupload=derive_device=videotoolbox"); + subFilters.Add("hwupload"); overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0"); } + if (usingHwSurface) + { + return (mainFilters, subFilters, overlayFilters); + } + + // For old jellyfin-ffmpeg that has broken hwsurface, add a hwupload var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) || subFilters.Any(f => !string.IsNullOrEmpty(f)) || overlayFilters.Any(f => !string.IsNullOrEmpty(f)); - - // This is a workaround for ffmpeg's hwupload implementation - // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame - // will cause the encoder to produce incorrect frames. if (needFiltering) { // INPUT videotoolbox/memory surface(vram/uma) // this will pass-through automatically if in/out format matches. mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld"); - mainFilters.Insert(0, "hwupload=derive_device=videotoolbox"); + mainFilters.Insert(0, "hwupload"); } return (mainFilters, subFilters, overlayFilters); @@ -6458,22 +6452,20 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); - // VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases. - // For example: https://trac.ffmpeg.org/ticket/10884 - // Disable it for now. - const bool UseHwSurface = false; + // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment. + bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported(); if (is8bitSwFormatsVt) { if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) || string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "h264", bitDepth, UseHwSurface); + return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface); } if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "vp8", bitDepth, UseHwSurface); + return GetHwaccelType(state, options, "vp8", bitDepth, useHwSurface); } } @@ -6482,12 +6474,12 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "hevc", bitDepth, UseHwSurface); + return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "vp9", bitDepth, UseHwSurface); + return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); } } @@ -7172,7 +7164,10 @@ namespace MediaBrowser.Controller.MediaEncoding var channels = state.OutputAudioChannels; - if (channels.HasValue && ((channels.Value != 2 && state.AudioStream?.Channels != 6) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)) + var useDownMixAlgorithm = state.AudioStream is not null + && DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream))); + + if (channels.HasValue && !useDownMixAlgorithm) { args += " -ac " + channels.Value; } diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index 952dd48703..cfff3eb144 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -2,9 +2,7 @@ #pragma warning disable CA1002, CA2227, CS1591 -using System; using System.Collections.Generic; -using System.Globalization; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; @@ -33,8 +31,6 @@ namespace MediaBrowser.Controller.Providers set => _remoteImages = value; } - public List UserDataList { get; set; } - public List People { get; set; } public bool HasMetadata { get; set; } @@ -68,32 +64,5 @@ namespace MediaBrowser.Controller.Providers People.Clear(); } } - - public UserItemData GetOrAddUserData(string userId) - { - UserDataList ??= new List(); - - UserItemData userData = null; - - foreach (var i in UserDataList) - { - if (string.Equals(userId, i.UserId.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) - { - userData = i; - } - } - - if (userData is null) - { - userData = new UserItemData() - { - UserId = new Guid(userId) - }; - - UserDataList.Add(userData); - } - - return userData; - } } } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index a7e027d94a..e4ac59b676 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -365,10 +365,7 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "CollectionNumber": var tmdbCollection = reader.ReadNormalizedString(); - if (!string.IsNullOrEmpty(tmdbCollection)) - { - item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection); - } + item.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbCollection); break; @@ -502,10 +499,7 @@ namespace MediaBrowser.LocalMetadata.Parsers if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) { var id = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(id)) - { - item.SetProviderId(providerIdValue, id); - } + item.TrySetProviderId(providerIdValue, id); } else { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index f684069102..334796f585 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1325,38 +1325,23 @@ namespace MediaBrowser.MediaEncoding.Probing // These support multiple values, but for now we only store the first. var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID")); - if (!string.IsNullOrEmpty(mb)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb); - } + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb); mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID")); - if (!string.IsNullOrEmpty(mb)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb); - } + audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, mb); mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID")); - if (!string.IsNullOrEmpty(mb)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb); - } + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, mb); mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID")); - if (!string.IsNullOrEmpty(mb)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb); - } + audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb); mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id")) ?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID")); - if (!string.IsNullOrEmpty(mb)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb); - } + audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, mb); } private string GetMultipleMusicBrainzId(string value) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index d37528ede8..d2715e2ac7 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -908,7 +908,18 @@ namespace MediaBrowser.Model.Dlna } } - var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)); + var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault(); + + var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null; + + var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null; + + if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null) + { + playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported; + playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels; + } + playlistItem.AudioCodecs = audioCodecs; if (directAudioStream is not null) { @@ -971,7 +982,7 @@ namespace MediaBrowser.Model.Dlna } // Honor requested max channels - playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels; + playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : options.MaxAudioChannels; int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem); playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate); @@ -1293,7 +1304,7 @@ namespace MediaBrowser.Model.Dlna // Check audio codec MediaStream? selectedAudioStream = null; - if (candidateAudioStreams.Any()) + if (candidateAudioStreams.Count != 0) { selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec)); if (selectedAudioStream is null) diff --git a/MediaBrowser.Model/Dto/UserItemDataDto.cs b/MediaBrowser.Model/Dto/UserItemDataDto.cs index adb2cd2ab3..3bb45a0e04 100644 --- a/MediaBrowser.Model/Dto/UserItemDataDto.cs +++ b/MediaBrowser.Model/Dto/UserItemDataDto.cs @@ -1,4 +1,3 @@ -#nullable disable using System; namespace MediaBrowser.Model.Dto @@ -66,12 +65,12 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the key. /// /// The key. - public string Key { get; set; } + public required string Key { get; set; } /// /// Gets or sets the item identifier. /// /// The item identifier. - public string ItemId { get; set; } + public Guid ItemId { get; set; } } } diff --git a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs index 385cd6a34e..0c03d430dc 100644 --- a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs +++ b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs @@ -1,8 +1,7 @@ namespace MediaBrowser.Model.Entities; /// -/// An enum representing an algorithm to downmix 6ch+ to stereo. -/// Algorithms sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620. +/// An enum representing an algorithm to downmix surround sound to stereo. /// public enum DownMixStereoAlgorithms { @@ -13,11 +12,24 @@ public enum DownMixStereoAlgorithms /// /// Algorithm by Dave_750. + /// Sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620. /// Dave750 = 1, /// /// Nightmode Dialogue algorithm. + /// Sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620. /// - NightmodeDialogue = 2 + NightmodeDialogue = 2, + + /// + /// RFC7845 Section 5.1.1.5 defined algorithm. + /// + Rfc7845 = 3, + + /// + /// AC-4 standard algorithm with its default gain values. + /// Defined in ETSI TS 103 190 Section 6.2.17. + /// + Ac4 = 4 } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 844214fae2..7e227c5aa1 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -202,7 +202,7 @@ namespace MediaBrowser.Model.Entities || dvProfile == 8 || dvProfile == 9)) { - var title = "DV Profile " + dvProfile; + var title = "Dolby Vision Profile " + dvProfile; if (dvBlCompatId > 0) { @@ -214,6 +214,7 @@ namespace MediaBrowser.Model.Entities 1 => title + " (HDR10)", 2 => title + " (SDR)", 4 => title + " (HLG)", + 6 => title + " (HDR10)", // Technically means Blu-ray, but practically always HDR10 _ => title }; } @@ -336,7 +337,11 @@ namespace MediaBrowser.Model.Entities attributes.Add(Codec.ToUpperInvariant()); } - if (VideoRange != VideoRange.Unknown) + if (VideoDoViTitle is not null) + { + attributes.Add(VideoDoViTitle); + } + else if (VideoRange != VideoRange.Unknown) { attributes.Add(VideoRange.ToString()); } diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs index 1c73091f0d..479ec7712d 100644 --- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs +++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs @@ -3,177 +3,214 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// +/// Class ProviderIdsExtensions. +/// +public static class ProviderIdsExtensions { /// - /// Class ProviderIdsExtensions. + /// Case insensitive dictionary of string representation. + /// + private static readonly Dictionary _metadataProviderEnumDictionary = + Enum.GetValues() + .ToDictionary( + enumValue => enumValue.ToString(), + enumValue => enumValue.ToString(), + StringComparer.OrdinalIgnoreCase); + + /// + /// Checks if this instance has an id for the given provider. + /// + /// The instance. + /// The of the provider name. + /// true if a provider id with the given name was found; otherwise false. + public static bool HasProviderId(this IHasProviderIds instance, string name) + => instance.TryGetProviderId(name, out _); + + /// + /// Checks if this instance has an id for the given provider. + /// + /// The instance. + /// The provider. + /// true if a provider id with the given name was found; otherwise false. + public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider) + => instance.HasProviderId(provider.ToString()); + + /// + /// Gets a provider id. /// - public static class ProviderIdsExtensions + /// The instance. + /// The name. + /// The provider id. + /// true if a provider id with the given name was found; otherwise false. + public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id) { - /// - /// Case insensitive dictionary of string representation. - /// - private static readonly Dictionary _metadataProviderEnumDictionary = - Enum.GetValues() - .ToDictionary( - enumValue => enumValue.ToString(), - enumValue => enumValue.ToString(), - StringComparer.OrdinalIgnoreCase); - - /// - /// Checks if this instance has an id for the given provider. - /// - /// The instance. - /// The of the provider name. - /// true if a provider id with the given name was found; otherwise false. - public static bool HasProviderId(this IHasProviderIds instance, string name) - { - ArgumentNullException.ThrowIfNull(instance); + ArgumentNullException.ThrowIfNull(instance); - return instance.TryGetProviderId(name, out _); + if (instance.ProviderIds is null) + { + id = null; + return false; } - /// - /// Checks if this instance has an id for the given provider. - /// - /// The instance. - /// The provider. - /// true if a provider id with the given name was found; otherwise false. - public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider) + var foundProviderId = instance.ProviderIds.TryGetValue(name, out id); + // This occurs when searching with Identify (and possibly in other places) + if (string.IsNullOrEmpty(id)) { - return instance.HasProviderId(provider.ToString()); + id = null; + foundProviderId = false; } - /// - /// Gets a provider id. - /// - /// The instance. - /// The name. - /// The provider id. - /// true if a provider id with the given name was found; otherwise false. - public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id) + return foundProviderId; + } + + /// + /// Gets a provider id. + /// + /// The instance. + /// The provider. + /// The provider id. + /// true if a provider id with the given name was found; otherwise false. + public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id) + { + return instance.TryGetProviderId(provider.ToString(), out id); + } + + /// + /// Gets a provider id. + /// + /// The instance. + /// The name. + /// System.String. + public static string? GetProviderId(this IHasProviderIds instance, string name) + { + instance.TryGetProviderId(name, out string? id); + return id; + } + + /// + /// Gets a provider id. + /// + /// The instance. + /// The provider. + /// System.String. + public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider) + { + return instance.GetProviderId(provider.ToString()); + } + + /// + /// Sets a provider id. + /// + /// The instance. + /// The name, this should not contain a '=' character. + /// The value. + /// Due to how deserialization from the database works the name can not contain '='. + /// true if the provider id got set successfully; otherwise, false. + public static bool TrySetProviderId(this IHasProviderIds instance, string? name, string? value) + { + ArgumentNullException.ThrowIfNull(instance); + + // When name contains a '=' it can't be deserialized from the database + if (string.IsNullOrWhiteSpace(name) + || string.IsNullOrWhiteSpace(value) + || name.Contains('=', StringComparison.Ordinal)) { - ArgumentNullException.ThrowIfNull(instance); - - if (instance.ProviderIds is null) - { - id = null; - return false; - } - - var foundProviderId = instance.ProviderIds.TryGetValue(name, out id); - // This occurs when searching with Identify (and possibly in other places) - if (string.IsNullOrEmpty(id)) - { - id = null; - foundProviderId = false; - } - - return foundProviderId; + return false; } - /// - /// Gets a provider id. - /// - /// The instance. - /// The provider. - /// The provider id. - /// true if a provider id with the given name was found; otherwise false. - public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id) + // Ensure it exists + instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Match on internal MetadataProvider enum string values before adding arbitrary providers + if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue)) { - return instance.TryGetProviderId(provider.ToString(), out id); + instance.ProviderIds[enumValue] = value; } - - /// - /// Gets a provider id. - /// - /// The instance. - /// The name. - /// System.String. - public static string? GetProviderId(this IHasProviderIds instance, string name) + else { - instance.TryGetProviderId(name, out string? id); - return id; + instance.ProviderIds[name] = value; } - /// - /// Gets a provider id. - /// - /// The instance. - /// The provider. - /// System.String. - public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider) + return true; + } + + /// + /// Sets a provider id. + /// + /// The instance. + /// The provider. + /// The value. + /// true if the provider id got set successfully; otherwise, false. + public static bool TrySetProviderId(this IHasProviderIds instance, MetadataProvider provider, string? value) + => instance.TrySetProviderId(provider.ToString(), value); + + /// + /// Sets a provider id. + /// + /// The instance. + /// The name, this should not contain a '=' character. + /// The value. + /// Due to how deserialization from the database works the name can not contain '='. + public static void SetProviderId(this IHasProviderIds instance, string name, string value) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentException.ThrowIfNullOrWhiteSpace(value); + + // When name contains a '=' it can't be deserialized from the database + if (name.Contains('=', StringComparison.Ordinal)) { - return instance.GetProviderId(provider.ToString()); + throw new ArgumentException("Provider id name cannot contain '='", nameof(name)); } - /// - /// Sets a provider id. - /// - /// The instance. - /// The name, this should not contain a '=' character. - /// The value. - /// Due to how deserialization from the database works the name can not contain '='. - public static void SetProviderId(this IHasProviderIds instance, string name, string value) + // Ensure it exists + instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Match on internal MetadataProvider enum string values before adding arbitrary providers + if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue)) { - ArgumentNullException.ThrowIfNull(instance); - ArgumentException.ThrowIfNullOrEmpty(name); - ArgumentException.ThrowIfNullOrEmpty(value); - - // When name contains a '=' it can't be deserialized from the database - if (name.Contains('=', StringComparison.Ordinal)) - { - throw new ArgumentException("Provider id name cannot contain '='", nameof(name)); - } - - // Ensure it exists - instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Match on internal MetadataProvider enum string values before adding arbitrary providers - if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue)) - { - instance.ProviderIds[enumValue] = value; - } - else - { - instance.ProviderIds[name] = value; - } + instance.ProviderIds[enumValue] = value; } - - /// - /// Sets a provider id. - /// - /// The instance. - /// The provider. - /// The value. - public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value) + else { - instance.SetProviderId(provider.ToString(), value); + instance.ProviderIds[name] = value; } + } - /// - /// Removes a provider id. - /// - /// The instance. - /// The name. - public static void RemoveProviderId(this IHasProviderIds instance, string name) - { - ArgumentNullException.ThrowIfNull(instance); - ArgumentException.ThrowIfNullOrEmpty(name); + /// + /// Sets a provider id. + /// + /// The instance. + /// The provider. + /// The value. + public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value) + => instance.SetProviderId(provider.ToString(), value); - instance.ProviderIds?.Remove(name); - } + /// + /// Removes a provider id. + /// + /// The instance. + /// The name. + public static void RemoveProviderId(this IHasProviderIds instance, string name) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentException.ThrowIfNullOrEmpty(name); - /// - /// Removes a provider id. - /// - /// The instance. - /// The provider. - public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider) - { - ArgumentNullException.ThrowIfNull(instance); + instance.ProviderIds?.Remove(name); + } - instance.ProviderIds?.Remove(provider.ToString()); - } + /// + /// Removes a provider id. + /// + /// The instance. + /// The provider. + public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider) + { + ArgumentNullException.ThrowIfNull(instance); + + instance.ProviderIds?.Remove(provider.ToString()); } } diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs index ea843f34cd..dd0d4fbfcf 100644 --- a/MediaBrowser.Model/Querying/QueryResult.cs +++ b/MediaBrowser.Model/Querying/QueryResult.cs @@ -1,47 +1,60 @@ -#nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; -namespace MediaBrowser.Model.Querying +namespace MediaBrowser.Model.Querying; + +/// +/// Query result container. +/// +/// The type of item contained in the query result. +public class QueryResult { - public class QueryResult + /// + /// Initializes a new instance of the class. + /// + public QueryResult() { - public QueryResult() - { - Items = Array.Empty(); - } + Items = Array.Empty(); + } - public QueryResult(IReadOnlyList items) - { - Items = items; - TotalRecordCount = items.Count; - } + /// + /// Initializes a new instance of the class. + /// + /// The list of items. + public QueryResult(IReadOnlyList items) + { + Items = items; + TotalRecordCount = items.Count; + } - public QueryResult(int? startIndex, int? totalRecordCount, IReadOnlyList items) - { - StartIndex = startIndex ?? 0; - TotalRecordCount = totalRecordCount ?? items.Count; - Items = items; - } + /// + /// Initializes a new instance of the class. + /// + /// The start index that was used to build the item list. + /// The total count of items. + /// The list of items. + public QueryResult(int? startIndex, int? totalRecordCount, IReadOnlyList items) + { + StartIndex = startIndex ?? 0; + TotalRecordCount = totalRecordCount ?? items.Count; + Items = items; + } - /// - /// Gets or sets the items. - /// - /// The items. - public IReadOnlyList Items { get; set; } + /// + /// Gets or sets the items. + /// + /// The items. + public IReadOnlyList Items { get; set; } - /// - /// Gets or sets the total number of records available. - /// - /// The total record count. - public int TotalRecordCount { get; set; } + /// + /// Gets or sets the total number of records available. + /// + /// The total record count. + public int TotalRecordCount { get; set; } - /// - /// Gets or sets the index of the first record in Items. - /// - /// First record index. - public int StartIndex { get; set; } - } + /// + /// Gets or sets the index of the first record in Items. + /// + /// First record index. + public int StartIndex { get; set; } } diff --git a/MediaBrowser.Model/Session/UserDataChangeInfo.cs b/MediaBrowser.Model/Session/UserDataChangeInfo.cs index 0fd24edccd..ccd768da51 100644 --- a/MediaBrowser.Model/Session/UserDataChangeInfo.cs +++ b/MediaBrowser.Model/Session/UserDataChangeInfo.cs @@ -1,4 +1,4 @@ -#nullable disable +using System; using MediaBrowser.Model.Dto; namespace MediaBrowser.Model.Session @@ -12,12 +12,12 @@ namespace MediaBrowser.Model.Session /// Gets or sets the user id. /// /// The user id. - public string UserId { get; set; } + public Guid UserId { get; set; } /// /// Gets or sets the user data list. /// /// The user data list. - public UserItemDataDto[] UserDataList { get; set; } + public required UserItemDataDto[] UserDataList { get; set; } } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index dfb6319acb..9a65852f02 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -23,7 +23,7 @@ - + diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 0083d4f75f..7e0773b6d3 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using ATL; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; @@ -18,7 +19,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; -using TagLib; namespace MediaBrowser.Providers.MediaInfo { @@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.MediaInfo /// public class AudioFileProber { + private const char InternalValueSeparator = '\u001F'; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly ILibraryManager _libraryManager; @@ -61,6 +62,7 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; _lyricResolver = lyricResolver; _lyricManager = lyricManager; + ATL.Settings.DisplayValueSeparator = InternalValueSeparator; } /// @@ -127,7 +129,6 @@ namespace MediaBrowser.Providers.MediaInfo audio.RunTimeTicks = mediaInfo.RunTimeTicks; audio.Size = mediaInfo.Size; - audio.PremiereDate = mediaInfo.PremiereDate; // Add external lyrics first to prevent the lrc file get overwritten on first scan var mediaStreams = new List(mediaInfo.MediaStreams); @@ -157,60 +158,23 @@ namespace MediaBrowser.Providers.MediaInfo /// Whether to extract embedded lyrics to lrc file. private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics) { - Tag? tags = null; - try - { - using var file = TagLib.File.Create(audio.Path); - var tagTypes = file.TagTypesOnDisk; + Track track = new Track(audio.Path); - if (tagTypes.HasFlag(TagTypes.Id3v2)) - { - tags = file.GetTag(TagTypes.Id3v2); - } - else if (tagTypes.HasFlag(TagTypes.Ape)) - { - tags = file.GetTag(TagTypes.Ape); - } - else if (tagTypes.HasFlag(TagTypes.FlacMetadata)) - { - tags = file.GetTag(TagTypes.FlacMetadata); - } - else if (tagTypes.HasFlag(TagTypes.Apple)) - { - tags = file.GetTag(TagTypes.Apple); - } - else if (tagTypes.HasFlag(TagTypes.Xiph)) - { - tags = file.GetTag(TagTypes.Xiph); - } - else if (tagTypes.HasFlag(TagTypes.AudibleMetadata)) - { - tags = file.GetTag(TagTypes.AudibleMetadata); - } - else if (tagTypes.HasFlag(TagTypes.Id3v1)) - { - tags = file.GetTag(TagTypes.Id3v1); - } - } - catch (Exception e) + // ATL will fall back to filename as title when it does not understand the metadata + if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT))) { - _logger.LogWarning(e, "TagLib-Sharp does not support this audio"); + track.Title = mediaInfo.Name; } - tags ??= new TagLib.Id3v2.Tag(); - tags.AlbumArtists ??= mediaInfo.AlbumArtists; - tags.Album ??= mediaInfo.Album; - tags.Title ??= mediaInfo.Name; - tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year; - tags.Performers ??= mediaInfo.Artists; - tags.Genres ??= mediaInfo.Genres; - tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track; - tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc; + track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; + track.Year ??= mediaInfo.ProductionYear; + track.TrackNumber ??= mediaInfo.IndexNumber; + track.DiscNumber ??= mediaInfo.ParentIndexNumber; if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List(); - var albumArtists = tags.AlbumArtists; + var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator); foreach (var albumArtist in albumArtists) { if (!string.IsNullOrEmpty(albumArtist)) @@ -223,7 +187,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - var performers = tags.Performers; + var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); foreach (var performer in performers) { if (!string.IsNullOrEmpty(performer)) @@ -236,7 +200,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - foreach (var composer in tags.Composers) + foreach (var composer in track.Composer.Split(InternalValueSeparator)) { if (!string.IsNullOrEmpty(composer)) { @@ -277,27 +241,32 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title)) + if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title)) { - audio.Name = tags.Title; + audio.Name = track.Title; } if (options.ReplaceAllMetadata) { - audio.Album = tags.Album; - audio.IndexNumber = Convert.ToInt32(tags.Track); - audio.ParentIndexNumber = Convert.ToInt32(tags.Disc); + audio.Album = track.Album; + audio.IndexNumber = track.TrackNumber; + audio.ParentIndexNumber = track.DiscNumber; } else { - audio.Album ??= tags.Album; - audio.IndexNumber ??= Convert.ToInt32(tags.Track); - audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc); + audio.Album ??= track.Album; + audio.IndexNumber ??= track.TrackNumber; + audio.ParentIndexNumber ??= track.DiscNumber; } - if (tags.Year != 0) + if (track.Date.HasValue) { - var year = Convert.ToInt32(tags.Year); + audio.PremiereDate = track.Date; + } + + if (track.Year.HasValue) + { + var year = track.Year.Value; audio.ProductionYear = year; if (!audio.PremiereDate.HasValue) @@ -308,64 +277,91 @@ namespace MediaBrowser.Providers.MediaInfo } catch (ArgumentOutOfRangeException ex) { - _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year); + _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year); } } } if (!audio.LockedFields.Contains(MetadataField.Genres)) { + var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 - ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray() + ? genres : audio.Genres; } - if (!double.IsNaN(tags.ReplayGainTrackGain)) + track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag); + + if (trackGainTag is not null) { - audio.NormalizationGain = (float)tags.ReplayGainTrackGain; + if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase)) + { + trackGainTag = trackGainTag[..^2].Trim(); + } + + if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) + { + audio.NormalizationGain = value; + } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) { - audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); + if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag) + || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag)) + && !string.IsNullOrEmpty(musicBrainzArtistTag)) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag); + } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); + if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag) + || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag)) + && !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag)) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag); + } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); + if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag) + || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag)) + && !string.IsNullOrEmpty(musicBrainzReleaseIdTag)) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag); + } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) { - audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); + if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag) + || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag)) + && !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag)) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag); + } } if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) { - // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`. - // See https://github.com/mono/taglib-sharp/issues/304 - var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack); - if (trackMbId is not null) + if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId) + || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId)) + && !string.IsNullOrEmpty(trackMbId)) { - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); + audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); } } // Save extracted lyrics if they exist, // and if the audio doesn't yet have lyrics. - if (!string.IsNullOrWhiteSpace(tags.Lyrics) + var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics; + if (!string.IsNullOrWhiteSpace(lyrics) && tryExtractEmbeddedLyrics) { - await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false); + await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index c750caa1c9..de0da7f7bd 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -220,10 +220,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.HomePageUrl = result.Website; } - if (!string.IsNullOrWhiteSpace(result.imdbID)) - { - item.SetProviderId(MetadataProvider.Imdb, result.imdbID); - } + item.TrySetProviderId(MetadataProvider.Imdb, result.imdbID); ParseAdditionalMetadata(itemResult, result, isEnglishRequested); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index dac7a74ed8..8d68e2dcfe 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -81,11 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies } remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); - - if (!string.IsNullOrWhiteSpace(movie.ImdbId)) - { - remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId); - } + remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId); return new[] { remoteResult }; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 5c6e71fd89..98c46895d7 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -56,10 +56,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People } result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); - if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId)) - { - result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); - } + result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); return new[] { result }; } @@ -129,11 +126,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People } item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); - - if (!string.IsNullOrEmpty(person.ImdbId)) - { - item.SetProviderId(MetadataProvider.Imdb, person.ImdbId); - } + item.TrySetProviderId(MetadataProvider.Imdb, person.ImdbId); result.HasMetadata = true; result.Item = item; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 489f5e2a17..e628abde55 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -187,20 +187,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV }; var externalIds = episodeResult.ExternalIds; - if (!string.IsNullOrEmpty(externalIds?.TvdbId)) - { - item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId); - } - - if (!string.IsNullOrEmpty(externalIds?.ImdbId)) - { - item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId); - } - - if (!string.IsNullOrEmpty(externalIds?.TvrageId)) - { - item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId); - } + item.TrySetProviderId(MetadataProvider.Tvdb, externalIds?.TvdbId); + item.TrySetProviderId(MetadataProvider.Imdb, externalIds?.ImdbId); + item.TrySetProviderId(MetadataProvider.TvRage, externalIds?.TvrageId); if (episodeResult.Videos?.Results is not null) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 10efb68b94..3f208b5993 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -73,10 +73,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV result.Item.Name = seasonResult.Name; } - if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId)) - { - result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId); - } + result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId); // TODO why was this disabled? var credits = seasonResult.Credits; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index d8476bd47d..e4062740fe 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -135,15 +135,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); if (series.ExternalIds is not null) { - if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId)) - { - remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId); - } + remoteResult.TrySetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId); - if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId)) - { - remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId); - } + remoteResult.TrySetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId); } remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); @@ -289,20 +283,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var ids = seriesResult.ExternalIds; if (ids is not null) { - if (!string.IsNullOrWhiteSpace(ids.ImdbId)) - { - series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId); - } - - if (!string.IsNullOrEmpty(ids.TvrageId)) - { - series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId); - } - - if (!string.IsNullOrEmpty(ids.TvdbId)) - { - series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId); - } + series.TrySetProviderId(MetadataProvider.Imdb, ids.ImdbId); + series.TrySetProviderId(MetadataProvider.TvRage, ids.TvrageId); + series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId); } var contentRatings = seriesResult.ContentRatings.Results ?? new List(); diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index d049c5a8ef..f2681500b0 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -572,10 +572,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var provider = reader.GetAttribute("type"); var providerId = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId)) - { - item.SetProviderId(provider, providerId); - } + item.TrySetProviderId(provider, providerId); break; case "thumb": @@ -604,10 +601,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue)) { var id = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(providerIdValue) && !string.IsNullOrWhiteSpace(id)) - { - item.SetProviderId(providerIdValue, id); - } + item.TrySetProviderId(providerIdValue, id); } else { diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 044efb51e9..2a1a14834b 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -59,80 +59,50 @@ namespace MediaBrowser.XbmcMetadata.Parsers try { // Extract episode details from the first episodedetails block - using (var stringReader = new StringReader(xml)) - using (var reader = XmlReader.Create(stringReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - FetchDataFromXmlNode(reader, item); - } - else - { - reader.Read(); - } - } - } + ReadEpisodeDetailsFromXml(item, xml, settings, cancellationToken); // Extract the last episode number from nfo - // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode + // Retrieves all additional episodedetails blocks from the rest of the nfo and concatenates the name, originalTitle and overview tags with the first episode // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag var name = new StringBuilder(item.Item.Name); + var originalTitle = new StringBuilder(item.Item.OriginalTitle); var overview = new StringBuilder(item.Item.Overview); while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1) { xml = xmlFile.Substring(0, index + srch.Length); xmlFile = xmlFile.Substring(index + srch.Length); - using (var stringReader = new StringReader(xml)) - using (var reader = XmlReader.Create(stringReader, settings)) + var additionalEpisode = new MetadataResult() + { + Item = new Episode() + }; + + // Extract episode details from additional episodedetails block + ReadEpisodeDetailsFromXml(additionalEpisode, xml, settings, cancellationToken); + + if (!string.IsNullOrEmpty(additionalEpisode.Item.Name)) + { + name.Append(" / ").Append(additionalEpisode.Item.Name); + } + + if (!string.IsNullOrEmpty(additionalEpisode.Item.Overview)) + { + overview.Append(" / ").Append(additionalEpisode.Item.Overview); + } + + if (!string.IsNullOrEmpty(additionalEpisode.Item.OriginalTitle)) + { + originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle); + } + + if (additionalEpisode.Item.IndexNumber != null) { - reader.MoveToContent(); - - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - case "title": - case "localtitle": - name.Append(" / ").Append(reader.ReadElementContentAsString()); - break; - case "episode": - { - if (int.TryParse(reader.ReadElementContentAsString(), out var num)) - { - item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); - } - - break; - } - - case "biography": - case "plot": - case "review": - overview.Append(" / ").Append(reader.ReadElementContentAsString()); - break; - } - } - - reader.Read(); - } + item.Item.IndexNumberEnd = Math.Max((int)additionalEpisode.Item.IndexNumber, item.Item.IndexNumberEnd ?? (int)additionalEpisode.Item.IndexNumber); } } item.Item.Name = name.ToString(); + item.Item.OriginalTitle = originalTitle.ToString(); item.Item.Overview = overview.ToString(); } catch (XmlException) @@ -200,5 +170,33 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } } + + /// + /// Reads the episode details from the given xml and saves the result in the provided result item. + /// + private void ReadEpisodeDetailsFromXml(MetadataResult item, string xml, XmlReaderSettings settings, CancellationToken cancellationToken) + { + using (var stringReader = new StringReader(xml)) + using (var reader = XmlReader.Create(stringReader, settings)) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + FetchDataFromXmlNode(reader, item); + } + else + { + reader.Read(); + } + } + } + } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index af867cd59f..2d65188b63 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -65,15 +65,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers tmdbId = contentId; } - if (!string.IsNullOrWhiteSpace(imdbId)) - { - item.SetProviderId(MetadataProvider.Imdb, imdbId); - } - - if (!string.IsNullOrWhiteSpace(tmdbId)) - { - item.SetProviderId(MetadataProvider.Tmdb, tmdbId); - } + item.TrySetProviderId(MetadataProvider.Imdb, imdbId); + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); break; } @@ -83,10 +76,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var movie = item as Movie; var tmdbcolid = reader.GetAttribute("tmdbcolid"); - if (!string.IsNullOrWhiteSpace(tmdbcolid) && movie is not null) - { - movie.SetProviderId(MetadataProvider.TmdbCollection, tmdbcolid); - } + movie?.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbcolid); var val = reader.ReadInnerXml(); diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index d99e11bcd9..59abef919e 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -48,29 +48,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers { case "id": { - string? imdbId = reader.GetAttribute("IMDB"); - string? tmdbId = reader.GetAttribute("TMDB"); - string? tvdbId = reader.GetAttribute("TVDB"); + item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB")); + item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB")); + string? tvdbId = reader.GetAttribute("TVDB"); if (string.IsNullOrWhiteSpace(tvdbId)) { tvdbId = reader.ReadElementContentAsString(); } - if (!string.IsNullOrWhiteSpace(imdbId)) - { - item.SetProviderId(MetadataProvider.Imdb, imdbId); - } - - if (!string.IsNullOrWhiteSpace(tmdbId)) - { - item.SetProviderId(MetadataProvider.Tmdb, tmdbId); - } - - if (!string.IsNullOrWhiteSpace(tvdbId)) - { - item.SetProviderId(MetadataProvider.Tvdb, tvdbId); - } + item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId); break; } diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs index 9954424a4c..85f327c934 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs @@ -54,11 +54,6 @@ namespace MediaBrowser.XbmcMetadata.Providers result.People = tmpItem.People; result.Images = tmpItem.Images; result.RemoteImages = tmpItem.RemoteImages; - - if (tmpItem.UserDataList is not null) - { - result.UserDataList = tmpItem.UserDataList; - } } /// diff --git a/jellyfin.ruleset b/jellyfin.ruleset index db116f46c8..67ffd9a37b 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -105,6 +105,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index 093970c38b..f657422a04 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -479,10 +479,7 @@ public class GuideManager : IGuideManager DateModified = DateTime.UtcNow }; - if (!string.IsNullOrEmpty(info.Etag)) - { - item.SetProviderId(EtagKey, info.Etag); - } + item.TrySetProviderId(EtagKey, info.Etag); } if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase)) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 6d88dbb8ef..31ddd427cc 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -51,8 +51,8 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -205,8 +205,8 @@ namespace Jellyfin.Model.Tests [InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 - [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 + [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450 // AndroidPixel [InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450 @@ -432,7 +432,14 @@ namespace Jellyfin.Model.Tests if (targetAudioStream?.IsExternal == false) { // Check expected audio codecs (1) - Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); + if ((why & TranscodeReason.AudioChannelsNotSupported) == 0) + { + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); + } + else + { + Assert.Equal(targetAudioStream.Channels, streamInfo.TranscodingMaxAudioChannels); + } } } else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs deleted file mode 100644 index 6773bbeb19..0000000000 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Emby.Naming.TV; -using Xunit; - -namespace Jellyfin.Naming.Tests.TV -{ - public class SeasonFolderTests - { - [Theory] - [InlineData("/Drive/Season 1", 1, true)] - [InlineData("/Drive/Season 2", 2, true)] - [InlineData("/Drive/Season 02", 2, true)] - [InlineData("/Drive/Seinfeld/S02", 2, true)] - [InlineData("/Drive/Seinfeld/2", 2, true)] - [InlineData("/Drive/Season 2009", 2009, true)] - [InlineData("/Drive/Season1", 1, true)] - [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] - [InlineData("/Drive/Season 7 (2016)", 7, false)] - [InlineData("/Drive/Staffel 7 (2016)", 7, false)] - [InlineData("/Drive/Stagione 7 (2016)", 7, false)] - [InlineData("/Drive/Season (8)", null, false)] - [InlineData("/Drive/3.Staffel", 3, false)] - [InlineData("/Drive/s06e05", null, false)] - [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] - [InlineData("/Drive/extras", 0, true)] - [InlineData("/Drive/specials", 0, true)] - public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory) - { - var result = SeasonPathParser.Parse(path, true, true); - - Assert.Equal(result.SeasonNumber is not null, result.Success); - Assert.Equal(result.SeasonNumber, seasonNumber); - Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); - } - } -} diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs new file mode 100644 index 0000000000..3a042df683 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -0,0 +1,37 @@ +using Emby.Naming.TV; +using Xunit; + +namespace Jellyfin.Naming.Tests.TV; + +public class SeasonPathParserTests +{ + [Theory] + [InlineData("/Drive/Season 1", 1, true)] + [InlineData("/Drive/s1", 1, true)] + [InlineData("/Drive/S1", 1, true)] + [InlineData("/Drive/Season 2", 2, true)] + [InlineData("/Drive/Season 02", 2, true)] + [InlineData("/Drive/Seinfeld/S02", 2, true)] + [InlineData("/Drive/Seinfeld/2", 2, true)] + [InlineData("/Drive/Seinfeld - S02", 2, true)] + [InlineData("/Drive/Season 2009", 2009, true)] + [InlineData("/Drive/Season1", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] + [InlineData("/Drive/Season 7 (2016)", 7, false)] + [InlineData("/Drive/Staffel 7 (2016)", 7, false)] + [InlineData("/Drive/Stagione 7 (2016)", 7, false)] + [InlineData("/Drive/Season (8)", null, false)] + [InlineData("/Drive/3.Staffel", 3, false)] + [InlineData("/Drive/s06e05", null, false)] + [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] + [InlineData("/Drive/extras", 0, true)] + [InlineData("/Drive/specials", 0, true)] + public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory) + { + var result = SeasonPathParser.Parse(path, true, true); + + Assert.Equal(result.SeasonNumber is not null, result.Success); + Assert.Equal(result.SeasonNumber, seasonNumber); + Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); + } +} diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index c0d06116b5..3721d1f7ac 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -123,6 +123,30 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(2004, item.ProductionYear); } + [Fact] + public void Fetch_Valid_MultiEpisode_With_Missing_Tags_Success() + { + var result = new MetadataResult() + { + Item = new Episode() + }; + + _parser.Fetch(result, "Test Data/Stargate Atlantis S01E01-E04.nfo", CancellationToken.None); + + var item = result.Item; + // provided for episode 1, 3 and 4 + Assert.Equal("Rising / Hide and Seek / Thirty-Eight Minutes", item.Name); + // <originaltitle> provided for all episodes + Assert.Equal("Rising (1) / Rising (2) / Hide and Seek / Thirty-Eight Minutes", item.OriginalTitle); + Assert.Equal(1, item.IndexNumber); + Assert.Equal(4, item.IndexNumberEnd); + Assert.Equal(1, item.ParentIndexNumber); + // <plot> only provided for episode 1 + Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview); + Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate); + Assert.Equal(2004, item.ProductionYear); + } + [Fact] public void Parse_GivenFileWithThumbWithoutAspect_Success() { diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 0a153b9cc1..5bc4abd06d 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -53,7 +53,10 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var userData = new Mock<IUserDataManager>(); userData.Setup(x => x.GetUserData(_testUser, It.IsAny<BaseItem>())) - .Returns(new UserItemData()); + .Returns(new UserItemData() + { + Key = "Something" + }); var directoryService = new Mock<IDirectoryService>(); _localImageFileMetadata = new FileSystemMetadata() diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo index 56250c09a8..e95f5002a1 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo @@ -7,6 +7,18 @@ <thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb> <watched>false</watched> <rating>8.0</rating> + <actor> + <name>Joe Flanigan</name> + <role>John Sheppard</role> + <order>0</order> + <thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb> + </actor> + <actor> + <name>David Hewlett</name> + <role>Rodney McKay</role> + <order>1</order> + <thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb> + </actor> </episodedetails> <episodedetails> <title>Rising (2) @@ -17,4 +29,16 @@ https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg false 7.9 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo new file mode 100644 index 0000000000..7dee0110c4 --- /dev/null +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo @@ -0,0 +1,89 @@ + + Rising + Rising (1) + 1 + 1 + 2004-07-16 + A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. + https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg + false + 8.0 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + + + Rising (2) + 1 + 2 + 2004-07-16 + https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg + false + 7.9 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + + + Hide and Seek + Hide and Seek + 1 + 3 + 2004-07-23 + https://artworks.thetvdb.com/banners/episodes/70851/25335.jpg + false + 7.5 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + + + Thirty-Eight Minutes + Thirty-Eight Minutes + 1 + 4 + 2004-07-23 + https://artworks.thetvdb.com/banners/episodes/70851/25336.jpg + false + 7.5 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + +