diff --git a/Jellyfin.Plugin.Bangumi/BangumiApi.cs b/Jellyfin.Plugin.Bangumi/BangumiApi.cs index 75da032..97003a2 100644 --- a/Jellyfin.Plugin.Bangumi/BangumiApi.cs +++ b/Jellyfin.Plugin.Bangumi/BangumiApi.cs @@ -41,6 +41,11 @@ public async Task> SearchSubject(string keyword, CancellationToken return Subject.SortBySimilarity(list, keyword); } + public async Task GetSubject(int id, CancellationToken token) + { + return await GetSubject(id.ToString(), token); + } + public async Task GetSubject(string id, CancellationToken token) { var jsonString = await SendRequest($"https://api.bgm.tv/v0/subjects/{id}", token); @@ -52,7 +57,7 @@ public async Task> SearchSubject(string keyword, CancellationToken return await GetSubjectEpisodeList(seriesId, EpisodeType.Normal, episodeNumber, token); } - public async Task?> GetSubjectEpisodeList(string seriesId, EpisodeType type, int episodeNumber, CancellationToken token) + public async Task?> GetSubjectEpisodeList(string seriesId, EpisodeType? type, int episodeNumber, CancellationToken token) { var result = await GetSubjectEpisodeListWithOffset(seriesId, type, 0, token); if (result == null) @@ -98,9 +103,11 @@ public async Task> SearchSubject(string keyword, CancellationToken return result.Data; } - public async Task?> GetSubjectEpisodeListWithOffset(string seriesId, EpisodeType type, int offset, CancellationToken token) + public async Task?> GetSubjectEpisodeListWithOffset(string seriesId, EpisodeType? type, int offset, CancellationToken token) { - var url = $"https://api.bgm.tv/v0/episodes?subject_id={seriesId}&type={(int)type}&limit={PageSize}"; + var url = $"https://api.bgm.tv/v0/episodes?subject_id={seriesId}&limit={PageSize}"; + if (type != null) + url += $"&type={(int)type}"; if (offset > 0) url += $"&offset={offset}"; var jsonString = await SendRequest(url, token); diff --git a/Jellyfin.Plugin.Bangumi/Model/Episode.cs b/Jellyfin.Plugin.Bangumi/Model/Episode.cs index 1d77d5d..044b07b 100644 --- a/Jellyfin.Plugin.Bangumi/Model/Episode.cs +++ b/Jellyfin.Plugin.Bangumi/Model/Episode.cs @@ -28,7 +28,7 @@ public class Episode public double Index { get; set; } [JsonPropertyName("airdate")] - public string? AirDate { get; set; } + public string AirDate { get; set; } = ""; public string? Duration { get; set; } diff --git a/Jellyfin.Plugin.Bangumi/Providers/EpisodeProvider.cs b/Jellyfin.Plugin.Bangumi/Providers/EpisodeProvider.cs index 8ea63b7..cc0d4d5 100644 --- a/Jellyfin.Plugin.Bangumi/Providers/EpisodeProvider.cs +++ b/Jellyfin.Plugin.Bangumi/Providers/EpisodeProvider.cs @@ -6,11 +6,13 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Plugin.Bangumi.Model; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; +using Episode = MediaBrowser.Controller.Entities.TV.Episode; namespace Jellyfin.Plugin.Bangumi.Providers { @@ -34,7 +36,16 @@ public class EpisodeProvider : IRemoteMetadataProvider, IH new(@"(\d{2,})") }; - private static readonly Regex[] SpecialEpisodeFileNameRegex = { new("Special"), new("OVA"), new("OAD") }; + private static readonly Regex[] SpecialEpisodeFileNameRegex = + { + new("Special"), + new("OVA"), + new("OAD"), + new(@"SP\d+"), + new(@"PV\d+"), + new("(NC)?(OP|ED)") + }; + private readonly BangumiApi _api; private readonly ILibraryManager _libraryManager; private readonly ILogger _log; @@ -55,6 +66,7 @@ public EpisodeProvider(Plugin plugin, BangumiApi api, ILogger l public async Task> GetMetadata(EpisodeInfo info, CancellationToken token) { token.ThrowIfCancellationRequested(); + EpisodeType? type = null; Model.Episode? episode = null; var result = new MetadataResult { ResultLanguage = Constants.Language }; @@ -62,6 +74,15 @@ public async Task> GetMetadata(EpisodeInfo info, Cancell if (string.IsNullOrEmpty(fileName)) return result; + if (fileName.ToUpper().Contains("OP")) + type = EpisodeType.Opening; + else if (fileName.ToUpper().Contains("ED")) + type = EpisodeType.Ending; + else if (fileName.ToUpper().Contains("SP")) + type = EpisodeType.Special; + else if (fileName.ToUpper().Contains("PV")) + type = EpisodeType.Preview; + var seriesId = info.SeriesProviderIds?.GetValueOrDefault(Constants.ProviderName); var parent = _libraryManager.FindByPath(Path.GetDirectoryName(info.Path), true); @@ -80,7 +101,7 @@ public async Task> GetMetadata(EpisodeInfo info, Cancell { episode = await _api.GetEpisode(episodeId, token); if (episode != null) - if (!SpecialEpisodeFileNameRegex.Any(x => x.IsMatch(info.Path))) + if (episode.Type == EpisodeType.Normal && !SpecialEpisodeFileNameRegex.Any(x => x.IsMatch(info.Path))) if ($"{episode.ParentId}" != seriesId) { _log.LogWarning("episode #{Episode} is not belong to series #{Series}, ignored", episodeId, seriesId); @@ -101,10 +122,11 @@ public async Task> GetMetadata(EpisodeInfo info, Cancell if (episode == null) { - var episodeListData = await _api.GetSubjectEpisodeList(seriesId, episodeIndex.Value, token); + var episodeListData = await _api.GetSubjectEpisodeList(seriesId, type, episodeIndex.Value, token); if (episodeListData == null) return result; - episodeIndex = GuessEpisodeNumber(episodeIndex, fileName, episodeListData.Max(episode => episode.Order)); + if (type is null or EpisodeType.Normal) + episodeIndex = GuessEpisodeNumber(episodeIndex, fileName, episodeListData.Max(episode => episode.Order)); episode = episodeListData.Find(x => (int)x.Order == episodeIndex); } @@ -124,6 +146,7 @@ public async Task> GetMetadata(EpisodeInfo info, Cancell result.Item.OriginalTitle = episode.OriginalName; result.Item.IndexNumber = (int)episode.Order; result.Item.Overview = episode.Description; + result.Item.ParentIndexNumber = 1; if (parent is Season season) { @@ -131,6 +154,22 @@ public async Task> GetMetadata(EpisodeInfo info, Cancell result.Item.ParentIndexNumber = season.IndexNumber; } + if (episode.Type == EpisodeType.Normal) + return result; + + // mark episode as special + result.Item.ParentIndexNumber = 0; + + var series = await _api.GetSubject(episode.ParentId, token); + if (series == null) + return result; + + var seasonNumber = parent is Season ? parent.IndexNumber : 1; + if (string.Compare(episode.AirDate, series.AirDate, StringComparison.Ordinal) < 0) + result.Item.AirsBeforeEpisodeNumber = seasonNumber; + else + result.Item.AirsAfterSeasonNumber = seasonNumber; + return result; }