diff options
6 files changed, 115 insertions, 6 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 6fa057702c..2462a754ae 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -611,7 +611,11 @@ namespace Emby.Server.Implementations.Dto // For these types we can try to optimize and assume these values will be equal if (item is MusicAlbum || item is Season || item is Playlist) { - dto.ChildCount = dto.RecursiveItemCount; + if (dto.RecursiveItemCount > 0) + { + dto.ChildCount = dto.RecursiveItemCount; + } + var folderChildCount = folder.LinkedChildren.Length; // The default is an empty array, so we can't reliably use the count when it's empty if (folderChildCount > 0) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 6624d0125f..a8bd832cc8 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -129,6 +129,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var tmdbId = justName.GetAttributeValue("tmdbid"); item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); + + // Anime databases model a single cour as its own entry, so a multi-season + // series maps to one of these ids per season rather than one per series. + var anidbId = justName.GetAttributeValue("anidbid"); + item.TrySetProviderId("AniDB", anidbId); + + var aniListId = justName.GetAttributeValue("anilistid"); + item.TrySetProviderId("AniList", aniListId); + + var aniSearchId = justName.GetAttributeValue("anisearchid"); + item.TrySetProviderId("AniSearch", aniSearchId); } } } diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 5d0ef65842..49ebc45f06 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -106,5 +106,11 @@ "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка", - "CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." + "CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён.", + "LyricDownloadFailureFromForItem": "Не ўдалося загрузіць тэкст песні з {0} для {1}", + "NameExtraDeletedScene": "Выдаленая сцэна", + "NameExtraInterview": "Інтэрв'ю", + "NameExtraNumbered": "{0} {1}", + "NameExtraScene": "Сцэна", + "NameExtraTrailer": "Трэйлер" } diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json index 377ad8d69e..1a1c89da35 100644 --- a/Emby.Server.Implementations/Localization/Core/fo.json +++ b/Emby.Server.Implementations/Localization/Core/fo.json @@ -13,7 +13,7 @@ "FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}", "HeaderFavoriteEpisodes": "Yndispartar", "LabelIpAddressValue": "IP-atsetur: {0}", - "AuthenticationSucceededWithUserName": "{0} varð samgildur", + "AuthenticationSucceededWithUserName": "{0} var samgildur", "HeaderFavoriteShows": "Yndisrøðir", "HeaderLiveTV": "Beinleiðis sjónvarp", "HearingImpaired": "Hoyrnarveik", @@ -68,7 +68,7 @@ "NotificationOptionServerRestartRequired": "Tørvur er á ambætaraendurbyrjan", "TasksApplicationCategory": "Nýtsluskipan", "NotificationOptionApplicationUpdateAvailable": "Skipanardagføring er tøk", - "NotificationOptionApplicationUpdateInstalled": "Skipanardagføring varð innløgd", + "NotificationOptionApplicationUpdateInstalled": "Skipanardagføring var innløgd", "UserStoppedPlayingItemWithValues": "{0} er liðugur at spæla {1} á {2}", "HomeVideos": "Heimaupptøkur", "StartupEmbyServerIsLoading": "Jellyfin-ambætarin er undir byrjanarinnlesing. Vinaliga royn aftur um eitt bil.", @@ -118,7 +118,7 @@ "TaskMoveTrickplayImages": "Flyt Trickplay-myndagoymslustað", "TaskMoveTrickplayImagesDescription": "Flytur verandi trickplay-fílur sambært savnsstillingunum.", "NameExtraThemeVideo": "Eyðkenniskykmynd", - "NameExtraDeletedScene": "Úrtikin mynd (scena)", + "NameExtraDeletedScene": "Úrtikin mynd", "NameExtraScene": "Mynd (scena)", "NameExtraUnknown": "Eykatilfar", "Original": "Upprunalig(t/ur)" diff --git a/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs index 9c247d54b9..bdac59c013 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Emby.Server.Implementations.Dto; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; @@ -21,11 +23,13 @@ namespace Jellyfin.Server.Implementations.Tests.Dto; public class DtoServiceTests { private readonly Mock<ILibraryManager> _libraryManagerMock; + private readonly Mock<IUserDataManager> _userDataManagerMock; private readonly DtoService _dtoService; public DtoServiceTests() { _libraryManagerMock = new Mock<ILibraryManager>(); + _userDataManagerMock = new Mock<IUserDataManager>(); var imageProcessor = new Mock<IImageProcessor>(); // Deterministic tag derived from the image so each item gets a distinct, assertable tag. @@ -42,7 +46,7 @@ public class DtoServiceTests _dtoService = new DtoService( NullLogger<DtoService>.Instance, _libraryManagerMock.Object, - new Mock<IUserDataManager>().Object, + _userDataManagerMock.Object, imageProcessor.Object, new Mock<IProviderManager>().Object, new Mock<IRecordingsManager>().Object, @@ -105,6 +109,57 @@ public class DtoServiceTests Assert.Null(dto.ParentPrimaryImageItemId); } + [Fact] + public void GetBaseItemDtos_SeasonWithNoRealEpisodes_ReportsVirtualEpisodesAsChildCount() + { + // No episode has aired yet, so RecursiveItemCount is 0. ChildCount must still report the + // virtual episodes clients get back for the season. This deliberately does not track + // Season.IsVirtualItem: that flag is recomputed only on a full refresh, so a season can + // carry it while already holding real episodes. + var (season, user) = BuildSeason(playedCount: 0, totalCount: 0, childCount: 10); + var options = new DtoOptions(false) { EnableImages = false, Fields = [ItemFields.ChildCount, ItemFields.RecursiveItemCount] }; + + var dto = _dtoService.GetBaseItemDtos([season], options, user, skipVisibilityCheck: true)[0]; + + Assert.Equal(0, dto.RecursiveItemCount); + Assert.Equal(10, dto.ChildCount); + } + + [Fact] + public void GetBaseItemDtos_SeasonWithRealEpisodes_KeepsRecursiveItemCountAsChildCount() + { + var (season, user) = BuildSeason(playedCount: 2, totalCount: 9, childCount: 11); + var options = new DtoOptions(false) { EnableImages = false, Fields = [ItemFields.ChildCount, ItemFields.RecursiveItemCount] }; + + var dto = _dtoService.GetBaseItemDtos([season], options, user, skipVisibilityCheck: true)[0]; + + Assert.Equal(9, dto.RecursiveItemCount); + // The shortcut still wins over the batched child count, which also counts virtual episodes. + Assert.Equal(9, dto.ChildCount); + } + + private (Season Season, User User) BuildSeason(int playedCount, int totalCount, int childCount) + { + var user = new User("user", "auth-provider", "reset-provider"); + var season = new Season { Id = Guid.NewGuid(), Name = "Season 2", SeriesId = Guid.NewGuid() }; + + _userDataManagerMock + .Setup(x => x.GetUserDataBatch(It.IsAny<IReadOnlyList<BaseItem>>(), user)) + .Returns(new Dictionary<Guid, UserItemData> { [season.Id] = new UserItemData { Key = "key" } }); + _userDataManagerMock + .Setup(x => x.GetResumeUserDataBatch(It.IsAny<IReadOnlyList<BaseItem>>(), user)) + .Returns(new Dictionary<Guid, VersionResumeData>()); + + _libraryManagerMock + .Setup(x => x.GetPlayedAndTotalCountBatch(It.IsAny<IReadOnlyList<Guid>>(), user)) + .Returns(new Dictionary<Guid, (int Played, int Total)> { [season.Id] = (playedCount, totalCount) }); + _libraryManagerMock + .Setup(x => x.GetChildCountBatch(It.IsAny<IReadOnlyList<Guid>>(), It.IsAny<Guid?>())) + .Returns(new Dictionary<Guid, int> { [season.Id] = childCount }); + + return (season, user); + } + private (Episode Episode, Season Season, Series Series) BuildEpisode(bool seasonHasPoster, bool seriesHasPoster = true) { // Non-local (http) paths keep aspect-ratio resolution off the image processor and on the diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs index feb2d8a625..67d924d152 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs @@ -62,6 +62,36 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.Equal(expectedId, actualId); } + [Theory] + [InlineData("/media/Show/Season 01 [anidbid=11111]", "AniDB", "11111")] + [InlineData("/media/Show/Season 01 [anidbid-11111]", "AniDB", "11111")] + [InlineData("/media/Show/Season 02 [anilistid=22222]", "AniList", "22222")] + [InlineData("/media/Show/Season 02 (anilistid=22222)", "AniList", "22222")] + [InlineData("/media/Show/Season 03 [anisearchid=33333]", "AniSearch", "33333")] + public void Resolve_SeasonFolderWithAniProviderId_SetsProviderId(string path, string providerKey, string expectedId) + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(providerKey, out var actualId)); + Assert.Equal(expectedId, actualId); + } + [Fact] public void Resolve_SeasonFolderWithMultipleProviderIds_SetsAll() { @@ -140,6 +170,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.False(season.TryGetProviderId(MetadataProvider.Tvdb, out _)); Assert.False(season.TryGetProviderId(MetadataProvider.TvMaze, out _)); Assert.False(season.TryGetProviderId(MetadataProvider.Tmdb, out _)); + Assert.False(season.TryGetProviderId("AniDB", out _)); + Assert.False(season.TryGetProviderId("AniList", out _)); + Assert.False(season.TryGetProviderId("AniSearch", out _)); } } } |
