aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs11
-rw-r--r--Emby.Server.Implementations/Localization/Core/be.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json6
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceTests.cs57
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs33
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 _));
}
}
}