diff --git a/README.md b/README.md index 88dc9788..9e41f76d 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ compatible with what. | `2.x.x` | `10.8` | `4.1.2` | | `3.x.x` | `10.8` | `4.2.0` | | `4.0.0` — `4.1.1` | `10.9` | `4.2.2` | -| `4.2.0` — `4.x.x` | `10.9` | `4.2.2` — `5.0.0` | -| `dev` | `10.9` | `dev` | +| `4.2.0` — `4.2.2` | `10.9` | `4.2.2` — `5.0.0` | +| `5.x.x` | `10.10` | `5.0.0` | +| `dev` | `10.10` | `dev` | ### Official Repository diff --git a/Shokofin/API/Info/EpisodeInfo.cs b/Shokofin/API/Info/EpisodeInfo.cs index e7c43f34..61ca2bdf 100644 --- a/Shokofin/API/Info/EpisodeInfo.cs +++ b/Shokofin/API/Info/EpisodeInfo.cs @@ -14,14 +14,11 @@ public class EpisodeInfo public Episode.AniDB AniDB; - public Episode.TvDB? TvDB; - public EpisodeInfo(Episode episode) { Id = episode.IDs.Shoko.ToString(); ExtraType = Ordering.GetExtraType(episode.AniDBEntity); Shoko = episode; AniDB = episode.AniDBEntity; - TvDB = episode.TvDBEntityList?.FirstOrDefault(); } } diff --git a/Shokofin/API/Info/SeasonInfo.cs b/Shokofin/API/Info/SeasonInfo.cs index 69db7e2d..1caa6c68 100644 --- a/Shokofin/API/Info/SeasonInfo.cs +++ b/Shokofin/API/Info/SeasonInfo.cs @@ -18,8 +18,6 @@ public class SeasonInfo public readonly Series.AniDBWithDate AniDB; - public readonly Series.TvDB? TvDB; - public readonly SeriesType Type; /// @@ -254,7 +252,6 @@ public SeasonInfo(Series series, SeriesType? customType, IEnumerable ext ExtraIds = extraIds.ToArray(); Shoko = series; AniDB = series.AniDBEntity; - TvDB = series.TvDBEntityList.FirstOrDefault(); Type = type; EarliestImportedAt = earliestImportedAt; LastImportedAt = lastImportedAt; diff --git a/Shokofin/API/Models/Episode.cs b/Shokofin/API/Models/Episode.cs index 87d26ace..ef1a033d 100644 --- a/Shokofin/API/Models/Episode.cs +++ b/Shokofin/API/Models/Episode.cs @@ -8,7 +8,7 @@ public class Episode { /// /// All identifiers related to the episode entry, e.g. the Shoko, AniDB, - /// TvDB, etc. + /// TMDB, etc. /// public EpisodeIDs IDs { get; set; } = new(); @@ -47,13 +47,6 @@ public class Episode [JsonPropertyName("AniDB")] public AniDB AniDBEntity { get; set; } = new(); - /// - /// The entries, if - /// is included in the data to add. - /// - [JsonPropertyName("TvDB")] - public List TvDBEntityList { get; set; } = []; - /// /// File cross-references for the episode. /// @@ -83,32 +76,11 @@ public class AniDB public Rating Rating { get; set; } = new(); } - public class TvDB - { - [JsonPropertyName("Season")] - public int SeasonNumber { get; set; } - - [JsonPropertyName("Number")] - public int EpisodeNumber { get; set; } - - public string Description { get; set; } = string.Empty; - - public int? AirsAfterSeason { get; set; } - - public int? AirsBeforeSeason { get; set; } - - public int? AirsBeforeEpisode { get; set; } - - public Image Thumbnail { get; set; } = new(); - } - public class EpisodeIDs : IDs { public int ParentSeries { get; set; } public int AniDB { get; set; } - - public List TvDB { get; set; } = []; } } diff --git a/Shokofin/API/Models/Image.cs b/Shokofin/API/Models/Image.cs index 03145a0f..b390e27c 100644 --- a/Shokofin/API/Models/Image.cs +++ b/Shokofin/API/Models/Image.cs @@ -6,7 +6,7 @@ namespace Shokofin.API.Models; public class Image { /// - /// AniDB, TvDB, TMDB, etc. + /// AniDB, TMDB, etc. /// public ImageSource Source { get; set; } = ImageSource.AniDB; @@ -90,7 +90,8 @@ public enum ImageSource AniDB = 1, /// - /// + /// Deprecated, but kept until the next major release for backwards compatibility. + /// TODO: REMOVE THIS IN 6.0 /// TvDB = 2, diff --git a/Shokofin/API/Models/Series.cs b/Shokofin/API/Models/Series.cs index 3d949aa8..9d93cdc4 100644 --- a/Shokofin/API/Models/Series.cs +++ b/Shokofin/API/Models/Series.cs @@ -22,7 +22,7 @@ public class Series /// /// All identifiers related to the series entry, e.g. the Shoko, AniDB, - /// TvDB, etc. + /// TMDB, etc. /// public SeriesIDs IDs { get; set; } = new(); @@ -45,12 +45,6 @@ public class Series [JsonPropertyName("AniDB")] public AniDBWithDate AniDBEntity { get; set; } = new(); - /// - /// The TvDB entries, if any. - /// - [JsonPropertyName("TvDB")] - public List TvDBEntityList { get; set; }= []; - public SeriesSizes Sizes { get; set; } = new(); /// @@ -191,11 +185,6 @@ public DateTime? EndDate } } - public class TvDB - { - public string Description { get; set; } = string.Empty; - } - public class SeriesIDs : IDs { public int ParentGroup { get; set; } = 0; diff --git a/Shokofin/API/Models/Title.cs b/Shokofin/API/Models/Title.cs index 676bca5e..67f970dc 100644 --- a/Shokofin/API/Models/Title.cs +++ b/Shokofin/API/Models/Title.cs @@ -28,7 +28,7 @@ public class Title public bool IsDefault { get; set; } /// - /// AniDB, TvDB, AniList, etc. + /// AniDB, TMDB, AniList, etc. /// public string Source { get; set; } = "Unknown"; } diff --git a/Shokofin/API/ShokoAPIClient.cs b/Shokofin/API/ShokoAPIClient.cs index 138c993d..7d4bedff 100644 --- a/Shokofin/API/ShokoAPIClient.cs +++ b/Shokofin/API/ShokoAPIClient.cs @@ -22,27 +22,6 @@ public class ShokoAPIClient : IDisposable private readonly ILogger Logger; - private static ComponentVersion? ServerVersion => - Plugin.Instance.Configuration.ServerVersion; - - private static readonly DateTime EpisodeSeriesParentAddedDate = DateTime.Parse("2023-04-17T00:00:00.000Z"); - - private static bool UseEpisodeGetSeriesEndpoint => - ServerVersion != null && ((ServerVersion.ReleaseChannel == ReleaseChannel.Stable && ServerVersion.Version == new Version("4.2.2.0")) || (ServerVersion.ReleaseDate.HasValue && ServerVersion.ReleaseDate.Value < EpisodeSeriesParentAddedDate)); - - private static readonly DateTime StableCutOffDate = DateTime.Parse("2023-12-16T00:00:00.000Z"); - - private static bool UseOlderSeriesAndFileEndpoints => - ServerVersion != null && ((ServerVersion.ReleaseChannel == ReleaseChannel.Stable && ServerVersion.Version == new Version("4.2.2.0")) || (ServerVersion.ReleaseDate.HasValue && ServerVersion.ReleaseDate.Value < StableCutOffDate)); - - private static readonly DateTime ImportFolderCutOffDate = DateTime.Parse("2024-03-28T00:00:00.000Z"); - - private static bool UseOlderImportFolderFileEndpoints => - ServerVersion != null && ((ServerVersion.ReleaseChannel == ReleaseChannel.Stable && ServerVersion.Version == new Version("4.2.2.0")) || (ServerVersion.ReleaseDate.HasValue && ServerVersion.ReleaseDate.Value < ImportFolderCutOffDate)); - - public static bool AllowEpisodeImages => - ServerVersion is { } serverVersion && serverVersion.Version > new Version("4.2.2.0"); - private readonly GuardedMemoryCache _cache; public ShokoAPIClient(ILogger logger) @@ -270,9 +249,6 @@ public Task GetImageAsync(ImageSource imageSource, ImageTyp public Task GetFile(string id) { - if (UseOlderSeriesAndFileEndpoints) - return Get($"/api/v3/File/{id}?includeXRefs=true&includeDataFrom=AniDB"); - return Get($"/api/v3/File/{id}?include=XRefs&includeDataFrom=AniDB"); } @@ -288,19 +264,12 @@ public Task> GetFileByPath(string path) public async Task> GetFilesForSeries(string seriesId) { - if (UseOlderSeriesAndFileEndpoints) - return await Get>($"/api/v3/Series/{seriesId}/File?pageSize=0&includeXRefs=true&includeDataFrom=AniDB").ConfigureAwait(false); - var listResult = await Get>($"/api/v3/Series/{seriesId}/File?pageSize=0&include=XRefs&includeDataFrom=AniDB").ConfigureAwait(false); return listResult.List; } public async Task> GetFilesForImportFolder(int importFolderId, string subPath, int page = 1) { - if (UseOlderImportFolderFileEndpoints) { - return await Get>($"/api/v3/ImportFolder/{importFolderId}/File?page={page}&pageSize=100&includeXRefs=true").ConfigureAwait(false); - } - return await Get>($"/api/v3/ImportFolder/{importFolderId}/File?page={page}&folderPath={Uri.EscapeDataString(subPath)}&pageSize=1000&include=XRefs").ConfigureAwait(false); } @@ -347,42 +316,24 @@ public async Task ScrobbleFile(string fileId, string episodeId, string eve public Task GetEpisode(string id) { - return Get($"/api/v3/Episode/{id}?includeDataFrom=AniDB,TvDB&includeXRefs=true"); + return Get($"/api/v3/Episode/{id}?includeDataFrom=AniDB,TMDB&includeXRefs=true"); } public async Task GetEpisodeImages(string id) { try { - if (AllowEpisodeImages) { - var episodeImages = await Get($"/api/v3/Episode/{id}/Images"); - // If the episode has no 'movie' images, get the series images to compensate. - if (episodeImages.Posters.Count is 0) { - var episode1 = await GetEpisode(id); - var seriesImages1 = await GetSeriesImages(episode1.IDs.ParentSeries.ToString()) ?? new(); - - episodeImages.Posters = seriesImages1.Posters; - episodeImages.Logos = seriesImages1.Logos; - episodeImages.Banners = seriesImages1.Banners; - episodeImages.Backdrops = seriesImages1.Backdrops; - } - return episodeImages; - } - - var episode0 = await GetEpisode(id); - var seriesId0 = episode0.IDs.ParentSeries.ToString(); - if (UseEpisodeGetSeriesEndpoint) { - var series = await GetSeriesFromEpisode(id); - if (series != null) - seriesId0 = series.IDs.Shoko.ToString(); + var episodeImages = await Get($"/api/v3/Episode/{id}/Images"); + // If the episode has no 'movie' images, get the series images to compensate. + if (episodeImages.Posters.Count is 0) { + var episode1 = await GetEpisode(id); + var seriesImages1 = await GetSeriesImages(episode1.IDs.ParentSeries.ToString()) ?? new(); + + episodeImages.Posters = seriesImages1.Posters; + episodeImages.Logos = seriesImages1.Logos; + episodeImages.Banners = seriesImages1.Banners; + episodeImages.Backdrops = seriesImages1.Backdrops; } - var seriesImages0 = seriesId0 is not "0" ? await GetSeriesImages(seriesId0) ?? new() : new(); - return new() { - Banners = seriesImages0.Banners, - Backdrops = seriesImages0.Backdrops, - Posters = seriesImages0.Posters, - Logos = seriesImages0.Logos, - Thumbnails = episode0.TvDBEntityList.FirstOrDefault()?.Thumbnail is { } thumbnail ? [thumbnail] : [], - }; + return episodeImages; } catch (ApiException e) when (e.StatusCode == HttpStatusCode.NotFound) { return null; @@ -391,22 +342,22 @@ public Task GetEpisode(string id) public Task> GetEpisodesFromSeries(string seriesId) { - return Get>($"/api/v3/Series/{seriesId}/Episode?pageSize=0&includeHidden=true&includeMissing=true&includeDataFrom=AniDB,TvDB&includeXRefs=true"); + return Get>($"/api/v3/Series/{seriesId}/Episode?pageSize=0&includeHidden=true&includeMissing=true&includeDataFrom=AniDB,TMDB&includeXRefs=true"); } public Task GetSeries(string id) { - return Get($"/api/v3/Series/{id}?includeDataFrom=AniDB,TvDB"); + return Get($"/api/v3/Series/{id}?includeDataFrom=AniDB,TMDB"); } public Task GetSeriesFromEpisode(string id) { - return Get($"/api/v3/Episode/{id}/Series?includeDataFrom=AniDB,TvDB"); + return Get($"/api/v3/Episode/{id}/Series?includeDataFrom=AniDB,TMDB"); } public Task> GetSeriesInGroup(string groupID, int filterID = 0, bool recursive = false) { - return Get>($"/api/v3/Filter/{filterID}/Group/{groupID}/Series?recursive={recursive}&includeMissing=true&includeIgnored=false&includeDataFrom=AniDB,TvDB"); + return Get>($"/api/v3/Filter/{filterID}/Group/{groupID}/Series?recursive={recursive}&includeMissing=true&includeIgnored=false&includeDataFrom=AniDB,TMDB"); } public Task> GetSeriesCast(string id) diff --git a/Shokofin/Configuration/PluginConfiguration.cs b/Shokofin/Configuration/PluginConfiguration.cs index 19ae9867..9555d908 100644 --- a/Shokofin/Configuration/PluginConfiguration.cs +++ b/Shokofin/Configuration/PluginConfiguration.cs @@ -569,7 +569,6 @@ public PluginConfiguration() DescriptionProvider.Shoko, DescriptionProvider.AniDB, DescriptionProvider.TMDB, - DescriptionProvider.TvDB, ]; HideUnverifiedTags = true; TagSources = TagSource.ContentIndicators | TagSource.Dynamic | TagSource.DynamicCast | TagSource.DynamicEnding | TagSource.Elements | @@ -603,7 +602,7 @@ public PluginConfiguration() VFS_Threads = 4; VFS_AddReleaseGroup = false; VFS_AddResolution = false; - VFS_AttachRoot = false; + VFS_AttachRoot = true; VFS_Location = VirtualRootLocation.Default; VFS_CustomLocation = null; VFS_ResolveLinks = false; diff --git a/Shokofin/Pages/Settings.html b/Shokofin/Pages/Settings.html index dd328585..193694e3 100644 --- a/Shokofin/Pages/Settings.html +++ b/Shokofin/Pages/Settings.html @@ -320,18 +320,6 @@

TMDB | Follow metadata language in library

-
- -
-

TvDB | Follow metadata language in library

-
- -
The metadata providers to use as the source of descriptions for entities, in priority order.
@@ -1168,8 +1156,8 @@

Basic Settings

Determines how specials are placed within seasons. Warning: Modifying this setting requires a recreation (read as; delete existing then create a new) of any libraries using this plugin — otherwise you will have mixed metadata.
diff --git a/Shokofin/Plugin.cs b/Shokofin/Plugin.cs index 09714c2b..477a6cfb 100644 --- a/Shokofin/Plugin.cs +++ b/Shokofin/Plugin.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; @@ -211,6 +212,19 @@ public Plugin(UsageTracker usageTracker, IServerConfigurationManager configurati // Disable VFS if we can't create symbolic links on Windows and no configuration exists. if (!configExists && !CanCreateSymbolicLinks) { Configuration.VFS_Enabled = false; + // Remove TvDB from the list of description providers. + var index = Configuration.DescriptionSourceList.IndexOf(Text.DescriptionProvider.TvDB); + if (index != -1) { + var list = Configuration.DescriptionSourceList.ToList(); + list.RemoveAt(index); + Configuration.DescriptionSourceList = [.. list]; + } + index = Configuration.DescriptionSourceOrder.IndexOf(Text.DescriptionProvider.TvDB); + if (index != -1) { + var list = Configuration.DescriptionSourceOrder.ToList(); + list.RemoveAt(index); + Configuration.DescriptionSourceOrder = [.. list]; + } SaveConfiguration(); } } diff --git a/Shokofin/Providers/CustomBoxSetProvider.cs b/Shokofin/Providers/CustomBoxSetProvider.cs index 33783a15..fd9416ce 100644 --- a/Shokofin/Providers/CustomBoxSetProvider.cs +++ b/Shokofin/Providers/CustomBoxSetProvider.cs @@ -121,7 +121,7 @@ private async Task EnsureGroupCollectionIsCorrect(Folder collectionRoot, B private bool EnsureNoTmdbIdIsSet(BoxSet collection) { var willRemove = collection.HasProviderId(MetadataProvider.TmdbCollection); - collection.SetProviderId(MetadataProvider.TmdbCollection.ToString(), null); + collection.ProviderIds.Remove(MetadataProvider.TmdbCollection.ToString()); return willRemove; } diff --git a/Shokofin/Resolvers/ShokoResolver.cs b/Shokofin/Resolvers/ShokoResolver.cs index b25fb003..0db9be79 100644 --- a/Shokofin/Resolvers/ShokoResolver.cs +++ b/Shokofin/Resolvers/ShokoResolver.cs @@ -223,16 +223,7 @@ NamingOptions namingOptions File.Delete(keepFile); } - // TODO: uncomment the code snippet once we reach JF 10.10. - // return new() { Items = items, ExtraFiles = new() }; - - // TODO: Remove these two hacks once we have proper support for adding multiple series at once. - if (!items.Any(i => i is Movie) && items.Count > 0) { - fileInfoList.Clear(); - fileInfoList.AddRange(items.OrderBy(s => int.Parse(s.Path.GetAttributeValue(ShokoSeriesId.Name)!)).Select(s => FileSystem.GetFileSystemInfo(s.Path))); - } - - return new() { Items = items.Where(i => i is Movie).ToList(), ExtraFiles = items.OfType().Select(s => FileSystem.GetFileSystemInfo(s.Path)).ToList() }; + return new() { Items = items, ExtraFiles = [] }; } return null; diff --git a/Shokofin/Shokofin.csproj b/Shokofin/Shokofin.csproj index c731c4ab..b108f5c7 100644 --- a/Shokofin/Shokofin.csproj +++ b/Shokofin/Shokofin.csproj @@ -8,7 +8,7 @@ - + diff --git a/Shokofin/Sync/SyncExtensions.cs b/Shokofin/Sync/SyncExtensions.cs index 81777bed..ecb1a86b 100644 --- a/Shokofin/Sync/SyncExtensions.cs +++ b/Shokofin/Sync/SyncExtensions.cs @@ -93,11 +93,10 @@ public static UserItemData MergeWithFileUserStats(this UserItemData userData, Fi return userData; } - public static UserItemData ToUserData(this File.UserStats userStats, Video video, Guid userId) + public static UserItemData ToUserData(this File.UserStats userStats, Video video) { return new UserItemData { - UserId = userId, Key = video.GetUserDataKeys()[0], LastPlayedDate = null, }.MergeWithFileUserStats(userStats); diff --git a/Shokofin/Sync/UserDataSyncManager.cs b/Shokofin/Sync/UserDataSyncManager.cs index 9ef7a1f6..542f4759 100644 --- a/Shokofin/Sync/UserDataSyncManager.cs +++ b/Shokofin/Sync/UserDataSyncManager.cs @@ -467,11 +467,14 @@ public void OnItemAddedOrUpdated(object? sender, ItemChangeEventArgs e) private Task SyncSeries(Series series, UserConfiguration userConfig, UserItemData? userData, SyncDirection direction, string seriesId) { + var user = UserManager.GetUserById(userConfig.UserId); + if (user == null) { + return Task.CompletedTask; + } // Try to load the user-data if it was not provided - userData ??= UserDataManager.GetUserData(userConfig.UserId, series); + userData ??= UserDataManager.GetUserData(user, series); // Create some new user-data if none exists. userData ??= new UserItemData { - UserId = userConfig.UserId, Key = series.GetUserDataKeys()[0], }; @@ -482,11 +485,14 @@ private Task SyncSeries(Series series, UserConfiguration userConfig, UserItemDat private Task SyncSeason(Season season, UserConfiguration userConfig, UserItemData? userData, SyncDirection direction, string seriesId) { + var user = UserManager.GetUserById(userConfig.UserId); + if (user == null) { + return Task.CompletedTask; + } // Try to load the user-data if it was not provided - userData ??= UserDataManager.GetUserData(userConfig.UserId, season); + userData ??= UserDataManager.GetUserData(user, season); // Create some new user-data if none exists. userData ??= new UserItemData { - UserId = userConfig.UserId, Key = season.GetUserDataKeys()[0], }; @@ -497,15 +503,18 @@ private Task SyncSeason(Season season, UserConfiguration userConfig, UserItemDat private Task SyncVideo(Video video, UserConfiguration userConfig, UserItemData? userData, SyncDirection direction, string episodeId) { + var user = UserManager.GetUserById(userConfig.UserId); + if (user == null) { + return Task.CompletedTask; + } if (!userConfig.SyncRestrictedVideos && video.CustomRating == "XXX") { Logger.LogTrace("Skipped {SyncDirection} user data for video {VideoName}. (Episode={EpisodeId})", direction.ToString(), video.Name, episodeId); return Task.CompletedTask; } // Try to load the user-data if it was not provided - userData ??= UserDataManager.GetUserData(userConfig.UserId, video); + userData ??= UserDataManager.GetUserData(user, video); // Create some new user-data if none exists. userData ??= new UserItemData { - UserId = userConfig.UserId, Key = video.GetUserDataKeys()[0], LastPlayedDate = null, }; @@ -522,11 +531,15 @@ private Task SyncVideo(Video video, UserConfiguration userConfig, UserItemData? private async Task SyncVideo(Video video, UserConfiguration userConfig, SyncDirection direction, string fileId, string episodeId) { try { + var user = UserManager.GetUserById(userConfig.UserId); + if (user == null) { + return; + } if (!userConfig.SyncRestrictedVideos && video.CustomRating == "XXX") { Logger.LogTrace("Skipped {SyncDirection} user data for video {VideoName}. (File={FileId},Episode={EpisodeId})", direction.ToString(), video.Name, fileId, episodeId); return; } - var localUserStats = UserDataManager.GetUserData(userConfig.UserId, video); + var localUserStats = UserDataManager.GetUserData(user, video); var remoteUserStats = await APIClient.GetFileUserStats(fileId, userConfig.Token); bool isInSync = UserDataEqualsFileUserStats(localUserStats, remoteUserStats); Logger.LogInformation("{SyncDirection} user data for video {VideoName}. (User={UserId},File={FileId},Episode={EpisodeId},Local={HaveLocal},Remote={HaveRemote},InSync={IsInSync})", direction.ToString(), video.Name, userConfig.UserId, fileId, episodeId, localUserStats != null, remoteUserStats != null, isInSync); @@ -560,12 +573,12 @@ private async Task SyncVideo(Video video, UserConfiguration userConfig, SyncDire break; // Create a new local stats entry if there is no local entry. if (localUserStats == null) { - UserDataManager.SaveUserData(userConfig.UserId, video, localUserStats = remoteUserStats.ToUserData(video, userConfig.UserId), UserDataSaveReason.Import, CancellationToken.None); + UserDataManager.SaveUserData(user, video, localUserStats = remoteUserStats.ToUserData(video), UserDataSaveReason.Import, CancellationToken.None); Logger.LogDebug("{SyncDirection} user data for video {VideoName} successful. (User={UserId},File={FileId},Episode={EpisodeId})", SyncDirection.Import.ToString(), video.Name, userConfig.UserId, fileId, episodeId); } // Else merge the remote stats into the local stats entry. else if (!localUserStats.LastPlayedDate.HasValue || remoteUserStats.LastUpdatedAt > localUserStats.LastPlayedDate.Value) { - UserDataManager.SaveUserData(userConfig.UserId, video, localUserStats.MergeWithFileUserStats(remoteUserStats), UserDataSaveReason.Import, CancellationToken.None); + UserDataManager.SaveUserData(user, video, localUserStats.MergeWithFileUserStats(remoteUserStats), UserDataSaveReason.Import, CancellationToken.None); Logger.LogDebug("{SyncDirection} user data for video {VideoName} successful. (User={UserId},File={FileId},Episode={EpisodeId})", SyncDirection.Import.ToString(), video.Name, userConfig.UserId, fileId, episodeId); } break; @@ -599,7 +612,7 @@ private async Task SyncVideo(Video video, UserConfiguration userConfig, SyncDire } // Else import if the remote state is fresher then the local state. else if (localUserStats.LastPlayedDate.Value < remoteUserStats.LastUpdatedAt) { - UserDataManager.SaveUserData(userConfig.UserId, video, localUserStats.MergeWithFileUserStats(remoteUserStats), UserDataSaveReason.Import, CancellationToken.None); + UserDataManager.SaveUserData(user, video, localUserStats.MergeWithFileUserStats(remoteUserStats), UserDataSaveReason.Import, CancellationToken.None); Logger.LogDebug("{SyncDirection} user data for video {VideoName} successful. (User={UserId},File={FileId},Episode={EpisodeId})", SyncDirection.Import.ToString(), video.Name, userConfig.UserId, fileId, episodeId); } break; diff --git a/Shokofin/Tasks/MigrateEpisodeUserDataTask.cs b/Shokofin/Tasks/MigrateEpisodeUserDataTask.cs deleted file mode 100644 index 495eb812..00000000 --- a/Shokofin/Tasks/MigrateEpisodeUserDataTask.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; -using Shokofin.ExternalIds; -using Shokofin.Providers; -using Shokofin.Sync; - -namespace Shokofin.Tasks; - -/// -/// Migrate user watch data for episodes store in Jellyfin to the newest id namespace. -/// -public class MigrateEpisodeUserDataTask( - ILogger logger, - IUserDataManager userDataManager, - IUserManager userManager, - ILibraryManager libraryManager -) : IScheduledTask, IConfigurableScheduledTask -{ - private readonly ILogger _logger = logger; - - private readonly IUserDataManager _userDataManager = userDataManager; - - private readonly IUserManager _userManager = userManager; - - private readonly ILibraryManager _libraryManager = libraryManager; - - /// - public string Name => "Migrate Episode User Watch Data"; - - /// - public string Description => "Migrate user watch data for episodes store in Jellyfin to the newest id namespace."; - - /// - public string Category => "Shokofin"; - - /// - public string Key => "ShokoMigrateEpisodeUserDataTask"; - - /// - public bool IsHidden => false; - - /// - public bool IsEnabled => true; - - /// - public bool IsLogged => true; - - /// - public IEnumerable GetDefaultTriggers() - => []; - - /// - public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) - { - var foundEpisodeCount = 0; - var seriesDict = new Dictionary episodes)>(); - var users = _userManager.Users.ToList(); - var allEpisodes = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = [BaseItemKind.Episode], - HasAnyProviderId = new Dictionary { { ShokoFileId.Name, string.Empty } }, - IsFolder = false, - Recursive = true, - DtoOptions = new(false) { - EnableImages = false - }, - SourceTypes = [SourceType.Library], - IsVirtualItem = false, - }) - .OfType() - .ToList(); - _logger.LogDebug("Attempting to migrate user watch data across {EpisodeCount} episodes and {UserCount} users.", allEpisodes.Count, users.Count); - foreach (var episode in allEpisodes) { - cancellationToken.ThrowIfCancellationRequested(); - - if (!episode.ParentIndexNumber.HasValue || !episode.IndexNumber.HasValue || - !episode.TryGetProviderId(ShokoFileId.Name, out var fileId) || - episode.Series is not Series series || !series.TryGetProviderId(ShokoSeriesId.Name, out var seriesId)) - continue; - - if (!seriesDict.TryGetValue(seriesId, out var tuple)) - seriesDict[seriesId] = tuple = (series, []); - - tuple.episodes.Add(episode); - foundEpisodeCount++; - } - - _logger.LogInformation("Found {SeriesCount} series and {EpisodeCount} episodes across {AllEpisodeCount} total episodes to search for user watch data to migrate.", seriesDict.Count, foundEpisodeCount, allEpisodes.Count); - var savedCount = 0; - var numComplete = 0; - var numTotal = foundEpisodeCount * users.Count; - var userDataDict = users.ToDictionary(user => user, user => (_userDataManager.GetAllUserData(user.Id).DistinctBy(data => data.Key).ToDictionary(data => data.Key), new List())); - var userDataToRemove = new List(); - foreach (var (seriesId, (series, episodes)) in seriesDict) { - cancellationToken.ThrowIfCancellationRequested(); - - SeriesProvider.AddProviderIds(series, seriesId); - var seriesUserKeys = series.GetUserDataKeys(); - // 10.9 post-4.1 id format - var primaryKey = seriesUserKeys.First(); - var keysToSearch = seriesUserKeys.Skip(1) - // 10.9 pre-4.1 id format - .Prepend($"shoko://shoko-series={seriesId}") - // 10.8 id format - .Prepend($"INVALID-BUT-DO-NOT-TOUCH:{seriesId}") - .ToList(); - _logger.LogTrace("Migrating user watch data for series {SeriesName}. (Series={SeriesId},Primary={PrimaryKey},Search={SearchKeys})", series.Name, seriesId, primaryKey, keysToSearch); - foreach (var episode in episodes) { - cancellationToken.ThrowIfCancellationRequested(); - - if (!episode.TryGetProviderId(ShokoFileId.Name, out var fileId)) - continue; - - var suffix = episode.ParentIndexNumber!.Value.ToString("000", CultureInfo.InvariantCulture) + episode.IndexNumber!.Value.ToString("000", CultureInfo.InvariantCulture); - var videoUserDataKeys = (episode as Video).GetUserDataKeys(); - var episodeKeysToSearch = keysToSearch.Select(key => key + suffix).Prepend(primaryKey + suffix).Concat(videoUserDataKeys).ToList(); - _logger.LogTrace("Migrating user watch data for season {SeasonNumber}, episode {EpisodeNumber} - {EpisodeName}. (Series={SeriesId},File={FileId},Search={SearchKeys})", episode.ParentIndexNumber, episode.IndexNumber, episode.Name, seriesId, fileId, episodeKeysToSearch); - foreach (var (user, (dataDict, dataList)) in userDataDict) { - var userData = _userDataManager.GetUserData(user, episode); - foreach (var searchKey in episodeKeysToSearch) { - if (!dataDict.TryGetValue(searchKey, out var searchUserData)) - continue; - - if (userData.CopyFrom(searchUserData)) { - _logger.LogInformation("Found user data to migrate. (Series={SeriesId},File={FileId},Search={SearchKeys},Key={SearchKey},User={UserId})", seriesId, fileId, episodeKeysToSearch, searchKey, user.Id); - dataList.Add(userData); - savedCount++; - } - break; - } - - numComplete++; - double percent = numComplete; - percent /= numTotal; - - progress.Report(percent * 100); - } - } - } - - // Last attempt to cancel before we save all the changes. - cancellationToken.ThrowIfCancellationRequested(); - - _logger.LogDebug("Saving {UserDataCount} user watch data entries across {UserCount} users", savedCount, users.Count); - foreach (var (user, (dataDict, dataList)) in userDataDict) { - if (dataList.Count is 0) - continue; - - _userDataManager.SaveAllUserData(user.Id, dataList.ToArray(), CancellationToken.None); - } - _logger.LogInformation("Saved {UserDataCount} user watch data entries across {UserCount} users", savedCount, users.Count); - - progress.Report(100); - return Task.CompletedTask; - } -} diff --git a/Shokofin/Utils/Ordering.cs b/Shokofin/Utils/Ordering.cs index a91697ac..1c94cd55 100644 --- a/Shokofin/Utils/Ordering.cs +++ b/Shokofin/Utils/Ordering.cs @@ -105,7 +105,7 @@ public enum SpecialOrderType { InBetweenSeasonByAirDate = 4, /// - /// Place the specials in-between normal episodes based upon the data from TvDB or TMDB. + /// Place the specials in-between normal episodes based upon the data from TMDB. /// InBetweenSeasonByOtherData = 5, } @@ -162,11 +162,10 @@ public static (int?, int?, int?, bool) GetSpecialPlacement(ShowInfo showInfo, Se return (null, null, null, showInfo.IsSpecial(episodeInfo)); } - // Abort if episode is not a TvDB special or AniDB special + // Abort if episode is not a TMDB special or AniDB special if (!showInfo.IsSpecial(episodeInfo)) return (null, null, null, false); - int? episodeNumber = null; int seasonNumber = GetSeasonNumber(showInfo, seasonInfo, episodeInfo); int? airsBeforeEpisodeNumber = null; int? airsBeforeSeasonNumber = null; @@ -175,10 +174,10 @@ public static (int?, int?, int?, bool) GetSpecialPlacement(ShowInfo showInfo, Se default: airsAfterSeasonNumber = seasonNumber; break; + case SpecialOrderType.InBetweenSeasonMixed: case SpecialOrderType.InBetweenSeasonByAirDate: - byAirDate: // Reset the order if we come from `SpecialOrderType.InBetweenSeasonMixed`. - episodeNumber = null; + int? episodeNumber = null; if (seasonInfo.SpecialsBeforeEpisodes.Contains(episodeInfo.Id)) { airsBeforeSeasonNumber = seasonNumber; break; @@ -195,34 +194,7 @@ public static (int?, int?, int?, bool) GetSpecialPlacement(ShowInfo showInfo, Se airsAfterSeasonNumber = seasonNumber; } break; - case SpecialOrderType.InBetweenSeasonMixed: case SpecialOrderType.InBetweenSeasonByOtherData: - // We need to have TvDB/TMDB data in the first place to do this method. - if (episodeInfo.TvDB == null) { - if (order == SpecialOrderType.InBetweenSeasonMixed) goto byAirDate; - break; - } - - episodeNumber = episodeInfo.TvDB.AirsBeforeEpisode; - if (!episodeNumber.HasValue) { - if (episodeInfo.TvDB.AirsBeforeSeason.HasValue) { - airsBeforeSeasonNumber = seasonNumber; - break; - } - - if (order == SpecialOrderType.InBetweenSeasonMixed) goto byAirDate; - airsAfterSeasonNumber = seasonNumber; - break; - } - - var nextEpisode = seasonInfo.EpisodeList.FirstOrDefault(e => e.TvDB != null && e.TvDB.SeasonNumber == seasonNumber && e.TvDB.EpisodeNumber == episodeNumber); - if (nextEpisode != null) { - airsBeforeEpisodeNumber = GetEpisodeNumber(showInfo, seasonInfo, nextEpisode); - airsBeforeSeasonNumber = seasonNumber; - break; - } - - if (order == SpecialOrderType.InBetweenSeasonMixed) goto byAirDate; break; } diff --git a/Shokofin/Utils/Text.cs b/Shokofin/Utils/Text.cs index ee93b947..6453e45d 100644 --- a/Shokofin/Utils/Text.cs +++ b/Shokofin/Utils/Text.cs @@ -86,7 +86,8 @@ public enum DescriptionProvider { AniDB = 2, /// - /// Provide the description from TvDB. + /// Deprecated, but kept until the next major release for backwards compatibility. + /// TODO: REMOVE THIS IN 6.0 /// TvDB = 3, @@ -157,21 +158,18 @@ public static string GetDescription(ShowInfo show, string? metadataLanguage) => GetDescriptionByDict(new() { {DescriptionProvider.Shoko, show.Shoko?.Description ?? show.DefaultSeason.Shoko.Description}, {DescriptionProvider.AniDB, metadataLanguage is "en" ? show.DefaultSeason.AniDB.Description : null}, - {DescriptionProvider.TvDB, show.DefaultSeason.TvDB?.Description}, }); public static string GetDescription(SeasonInfo season, string? metadataLanguage) => GetDescriptionByDict(new() { {DescriptionProvider.Shoko, season.Shoko.Description}, {DescriptionProvider.AniDB, metadataLanguage is "en" ? season.AniDB.Description : null}, - {DescriptionProvider.TvDB, season.TvDB?.Description}, }); public static string GetDescription(EpisodeInfo episode, string? metadataLanguage) => GetDescriptionByDict(new() { {DescriptionProvider.Shoko, episode.Shoko.Description}, {DescriptionProvider.AniDB, metadataLanguage is "en" ? episode.AniDB.Description : null}, - {DescriptionProvider.TvDB, episode.TvDB?.Description}, }); public static string GetDescription(IEnumerable episodeList, string? metadataLanguage) @@ -201,8 +199,6 @@ private static string GetDescriptionByDict(Dictionary descriptions.TryGetValue(DescriptionProvider.AniDB, out var desc) ? SanitizeAnidbDescription(desc ?? string.Empty) : null, - DescriptionProvider.TvDB => - descriptions.TryGetValue(DescriptionProvider.TvDB, out var desc) ? desc : null, _ => null }; if (!string.IsNullOrEmpty(overview)) diff --git a/build.yaml b/build.yaml index c9b30e88..f9c6cc4a 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ name: "Shoko" guid: "5216ccbf-d24a-4eb3-8a7e-7da4230b7052" imageUrl: https://raw.githubusercontent.com/ShokoAnime/Shokofin/metadata/banner.png -targetAbi: "10.9.7.0" +targetAbi: "10.10.0.0" owner: "ShokoAnime" overview: "Manage your anime from Jellyfin using metadata from Shoko" description: >