diff --git a/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs b/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs index 54cdd58..f5b2b1c 100644 --- a/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs +++ b/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs @@ -24,7 +24,7 @@ public void TestAddMovie() { var libraryManagerStub = new Mock(); var scraperManager = new ScraperManager(loggerFactory); - scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object)); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory)); var fileSystemStub = new Mock(); var directoryServiceStub = new Mock(); @@ -59,7 +59,7 @@ public void TestUpdateMovie() { var libraryManagerStub = new Mock(); var scraperManager = new ScraperManager(loggerFactory); - scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object)); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory)); var fileSystemStub = new Mock(); var directoryServiceStub = new Mock(); @@ -96,7 +96,7 @@ public void TestAddSeason() { var libraryManagerStub = new Mock(); var scraperManager = new ScraperManager(loggerFactory); - scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object)); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory)); var fileSystemStub = new Mock(); var directoryServiceStub = new Mock(); @@ -134,7 +134,7 @@ public void TestGetMedia() try { var libraryManagerStub = new Mock(); - var api = new Mgtv(loggerFactory, libraryManagerStub.Object); + var api = new Mgtv(loggerFactory); var media = await api.GetMedia(new Season(), "514446"); Console.WriteLine(media); } diff --git a/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs b/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs index 1262bda..535e074 100644 --- a/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs +++ b/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs @@ -34,8 +34,9 @@ namespace Jellyfin.Plugin.Danmu; public class LibraryManagerEventsHelper : IDisposable { private readonly List _queuedEvents; - private readonly IMemoryCache _pendingAddEventCache; - private readonly MemoryCacheEntryOptions _expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + private readonly IMemoryCache _memoryCache; + private readonly MemoryCacheEntryOptions _pendingAddExpiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + private readonly MemoryCacheEntryOptions _danmuUpdatedExpiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; @@ -62,7 +63,7 @@ public PluginConfiguration Config public LibraryManagerEventsHelper(ILibraryManager libraryManager, ILoggerFactory loggerFactory, Jellyfin.Plugin.Danmu.Core.IFileSystem fileSystem, ScraperManager scraperManager) { _queuedEvents = new List(); - _pendingAddEventCache = new MemoryCache(new MemoryCacheOptions()); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); _libraryManager = libraryManager; _logger = loggerFactory.CreateLogger(); @@ -162,14 +163,14 @@ private async Task OnQueueTimerCallbackInternal() { case Movie when ev.EventType is EventType.Add: _logger.LogInformation("Movie add: {0}", ev.Item.Name); - _pendingAddEventCache.Set(ev.Item.Id, ev, _expiredOption); + _memoryCache.Set(ev.Item.Id, ev, _pendingAddExpiredOption); break; case Movie when ev.EventType is EventType.Update: _logger.LogInformation("Movie update: {0}", ev.Item.Name); - if (_pendingAddEventCache.TryGetValue(ev.Item.Id, out LibraryEvent addMovieEv)) + if (_memoryCache.TryGetValue(ev.Item.Id, out LibraryEvent addMovieEv)) { queuedMovieAdds.Add(addMovieEv); - _pendingAddEventCache.Remove(ev.Item.Id); + _memoryCache.Remove(ev.Item.Id); } else { @@ -198,14 +199,14 @@ private async Task OnQueueTimerCallbackInternal() break; case Season when ev.EventType is EventType.Add: _logger.LogInformation("Season add: {0}", ev.Item.Name); - _pendingAddEventCache.Set(ev.Item.Id, ev, _expiredOption); + _memoryCache.Set(ev.Item.Id, ev, _pendingAddExpiredOption); break; case Season when ev.EventType is EventType.Update: _logger.LogInformation("Season update: {0}", ev.Item.Name); - if (_pendingAddEventCache.TryGetValue(ev.Item.Id, out LibraryEvent addSeasonEv)) + if (_memoryCache.TryGetValue(ev.Item.Id, out LibraryEvent addSeasonEv)) { queuedSeasonAdds.Add(addSeasonEv); - _pendingAddEventCache.Remove(ev.Item.Id); + _memoryCache.Remove(ev.Item.Id); } else { @@ -466,9 +467,15 @@ public async Task ProcessQueuedSeasonEvents(IReadOnlyCollection ev var queueUpdateMeta = new List(); foreach (var season in seasons) { + // // 虚拟季第一次请求忽略 + // if (season.LocationType == LocationType.Virtual && season.IndexNumber is null) + // { + // continue; + // } + if (season.IndexNumber.HasValue && season.IndexNumber == 0) { - _logger.LogInformation("特典不处理:name={0} number={1}", season.Name, season.IndexNumber); + _logger.LogInformation("special特典文件夹不处理:name={0} number={1}", season.Name, season.IndexNumber); continue; } @@ -518,6 +525,12 @@ public async Task ProcessQueuedSeasonEvents(IReadOnlyCollection ev { foreach (var season in seasons) { + // // 虚拟季第一次请求忽略 + // if (season.LocationType == LocationType.Virtual && season.IndexNumber is null) + // { + // continue; + // } + var queueUpdateMeta = new List(); // GetEpisodes一定要取所有fields,要不然更新会导致重建虚拟season季信息 var episodes = season.GetEpisodes(null, new DtoOptions(true)); @@ -526,6 +539,14 @@ public async Task ProcessQueuedSeasonEvents(IReadOnlyCollection ev continue; } + // 不处理季文件夹下的特典和extras影片(动画经常会混在一起) + var episodesWithoutSP = episodes.Where(x => x.ParentIndexNumber != null && x.ParentIndexNumber > 0).ToList(); + if (episodes.Count != episodesWithoutSP.Count) + { + _logger.LogInformation("{0}季存在{1}个特典或extra片段,忽略处理.", season.Name, (episodes.Count - episodesWithoutSP.Count)); + episodes = episodesWithoutSP; + } + foreach (var scraper in _scraperManager.All()) { try @@ -545,16 +566,17 @@ public async Task ProcessQueuedSeasonEvents(IReadOnlyCollection ev foreach (var (episode, idx) in episodes.WithIndex()) { + var fileName = Path.GetFileName(episode.Path); var indexNumber = episode.IndexNumber ?? 0; if (indexNumber <= 0) { - _logger.LogInformation("[{0}]匹配失败,缺少集号. [{1}]{2}", scraper.Name, season.Name, episode.Name); + _logger.LogInformation("[{0}]匹配失败,缺少集号. [{1}]{2}", scraper.Name, season.Name, fileName); continue; } if (indexNumber > media.Episodes.Count) { - _logger.LogInformation("[{0}]匹配失败,集号超过总集数,可能集号错误. [{1}]{2} indexNumber: {3}", scraper.Name, season.Name, episode.Name, indexNumber); + _logger.LogInformation("[{0}]匹配失败,集号超过总集数,可能识别集号错误. [{1}]{2} indexNumber: {3}", scraper.Name, season.Name, fileName, indexNumber); continue; } @@ -697,10 +719,20 @@ public async Task ProcessQueuedEpisodeEvents(IReadOnlyCollection e var episodeList = season.GetEpisodes(null, new DtoOptions(true)); foreach (var (episode, idx) in episodeList.WithIndex()) { + var fileName = Path.GetFileName(episode.Path); + // 没对应剧集号的,忽略处理 var indexNumber = episode.IndexNumber ?? 0; if (indexNumber < 1 || indexNumber > media.Episodes.Count) { + _logger.LogInformation("[{0}]缺少集号或集号超过弹幕数,忽略处理. [{1}]{2}", scraper.Name, season.Name, fileName); + continue; + } + + // 特典或extras影片不处理(动画经常会放在季文件夹下) + if (episode.ParentIndexNumber is null or 0) + { + _logger.LogInformation("[{0}]缺少季号,可能是特典或extras影片,忽略处理. [{1}]{2}", scraper.Name, season.Name, fileName); continue; } @@ -757,17 +789,30 @@ public async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string c try { // 弹幕5分钟内更新过,忽略处理(有时Update事件会重复执行) - if (!ignoreCheck && IsRepeatAction(item)) + var checkDownloadedKey = $"{item.Id}_{commentId}"; + if (!ignoreCheck && _memoryCache.TryGetValue(checkDownloadedKey, out var latestDownloaded)) { - _logger.LogInformation("[{0}]最近5分钟已更新过弹幕xml,忽略处理:{1}", scraper.Name, item.Name); + _logger.LogInformation("[{0}]最近5分钟已更新过弹幕xml,忽略处理:{1}.{2}", scraper.Name, item.IndexNumber, item.Name); return; } + _memoryCache.Set(checkDownloadedKey, true, _danmuUpdatedExpiredOption); var danmaku = await scraper.GetDanmuContent(item, commentId); if (danmaku != null) { - await this.DownloadDanmuInternal(item, danmaku.ToXml()); - this._logger.LogInformation("[{0}]弹幕下载成功:name={1}", scraper.Name, item.Name); + var bytes = danmaku.ToXml(); + if (bytes.Length < 10 * 1024) + { + _memoryCache.Remove(checkDownloadedKey); + _logger.LogInformation("[{0}]弹幕内容少于10KB,忽略处理:{1}.{2}", scraper.Name, item.IndexNumber, item.Name); + return; + } + await this.SaveDanmu(item, bytes); + this._logger.LogInformation("[{0}]弹幕下载成功:name={1}.{2} commentId={3}", scraper.Name, item.IndexNumber, item.Name, commentId); + } + else + { + _memoryCache.Remove(checkDownloadedKey); } } catch (Exception ex) @@ -776,11 +821,12 @@ public async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string c } } - private bool IsRepeatAction(BaseItem item) + private bool IsRepeatAction(BaseItem item, string checkDownloadedKey) { // 单元测试时为null if (item.FileNameWithoutExtension == null) return false; + // 通过xml文件属性判断(多线程时判断有误) var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml"); if (!this._fileSystem.Exists(danmuPath)) { @@ -792,7 +838,7 @@ private bool IsRepeatAction(BaseItem item) return diff.TotalSeconds < 300; } - private async Task DownloadDanmuInternal(BaseItem item, byte[] bytes) + private async Task SaveDanmu(BaseItem item, byte[] bytes) { // 单元测试时为null if (item.FileNameWithoutExtension == null) return; diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs index a66dc93..1355c2c 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/Bilibili.cs @@ -302,7 +302,7 @@ private async Task GetMatchBiliSeasonId(BaseItem item, string searchName) var score = searchName.Distance(title); if (score < 0.7) { - log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); + log.LogDebug("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); continue; } @@ -310,7 +310,7 @@ private async Task GetMatchBiliSeasonId(BaseItem item, string searchName) var itemPubYear = item.ProductionYear ?? 0; if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear) { - log.LogInformation("[{0}] 发行年份不一致,忽略处理. b站:{1} jellyfin: {2}", title, pubYear, itemPubYear); + log.LogDebug("[{0}] 发行年份不一致,忽略处理. b站:{1} jellyfin: {2}", title, pubYear, itemPubYear); continue; } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs index 4a6dba9..30c6287 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Dandan/Dandan.cs @@ -97,7 +97,7 @@ public override async Task> Search(BaseItem item) var score = searchName.Distance(title); if (score < 0.7) { - log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); + log.LogDebug("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); continue; } @@ -105,7 +105,7 @@ public override async Task> Search(BaseItem item) var itemPubYear = item.ProductionYear ?? 0; if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear) { - log.LogInformation("[{0}] 发行年份不一致,忽略处理. dandan:{1} jellyfin: {2}", title, pubYear, itemPubYear); + log.LogDebug("[{0}] 发行年份不一致,忽略处理. dandan:{1} jellyfin: {2}", title, pubYear, itemPubYear); continue; } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs index c47a42d..623d0e7 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs @@ -100,7 +100,7 @@ public override async Task> Search(BaseItem item) var score = searchName.Distance(title); if (score < 0.7) { - log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); + log.LogDebug("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); continue; } @@ -108,7 +108,7 @@ public override async Task> Search(BaseItem item) var itemPubYear = item.ProductionYear ?? 0; if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear) { - log.LogInformation("[{0}] 发行年份不一致,忽略处理. Iqiyi:{1} jellyfin: {2}", title, pubYear, itemPubYear); + log.LogDebug("[{0}] 发行年份不一致,忽略处理. Iqiyi:{1} jellyfin: {2}", title, pubYear, itemPubYear); continue; } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs index b167e90..66a0966 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs @@ -24,13 +24,11 @@ public class Mgtv : AbstractScraper public const string ScraperProviderId = "MgtvID"; private readonly MgtvApi _api; - private readonly ILibraryManager _libraryManager; - public Mgtv(ILoggerFactory logManager, ILibraryManager libraryManager) + public Mgtv(ILoggerFactory logManager) : base(logManager.CreateLogger()) { _api = new MgtvApi(logManager); - _libraryManager = libraryManager; } public override int DefaultOrder => 6; @@ -112,7 +110,7 @@ public override async Task> Search(BaseItem item) var score = searchName.Distance(title); if (score < 0.7) { - log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); + log.LogDebug("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); continue; } @@ -120,7 +118,7 @@ public override async Task> Search(BaseItem item) var itemPubYear = item.ProductionYear ?? 0; if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear) { - log.LogInformation("[{0}] 发行年份不一致,忽略处理. year: {1} jellyfin: {2}", title, pubYear, itemPubYear); + log.LogDebug("[{0}] 发行年份不一致,忽略处理. year: {1} jellyfin: {2}", title, pubYear, itemPubYear); continue; } @@ -181,9 +179,8 @@ public override async Task> Search(BaseItem item) // 从季信息元数据中,获取cid值 - // 没SXX季文件夹时,GetParent是Series,有时,GetParent是Season,所以需要通过seasonId中获取 - var seasonId = ((MediaBrowser.Controller.Entities.TV.Episode)item).FindSeasonId(); - var season = _libraryManager.GetItemById(seasonId); + // 不能通过GetParent获取Season,因为没有SXX季文件夹时,GetParent是Series + var season = ((MediaBrowser.Controller.Entities.TV.Episode)item).Season; season.ProviderIds.TryGetValue(ScraperProviderId, out var cid); return new ScraperEpisode() { Id = id, CommentId = $"{cid},{id}" }; } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs index fcb24a3..3bc8fc0 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs @@ -107,7 +107,7 @@ public override async Task> Search(BaseItem item) var score = searchName.Distance(title); if (score < 0.7) { - log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); + log.LogDebug("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); continue; } @@ -115,7 +115,7 @@ public override async Task> Search(BaseItem item) var itemPubYear = item.ProductionYear ?? 0; if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear) { - log.LogInformation("[{0}] 发行年份不一致,忽略处理. year: {1} jellyfin: {2}", title, pubYear, itemPubYear); + log.LogDebug("[{0}] 发行年份不一致,忽略处理. year: {1} jellyfin: {2}", title, pubYear, itemPubYear); continue; } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs b/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs index 912cf1b..6662e7e 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs @@ -107,7 +107,7 @@ public override async Task> Search(BaseItem item) var score = searchName.Distance(title); if (score < 0.7) { - log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); + log.LogDebug("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score); continue; } @@ -115,7 +115,7 @@ public override async Task> Search(BaseItem item) var itemPubYear = item.ProductionYear ?? 0; if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear) { - log.LogInformation("[{0}] 发行年份不一致,忽略处理. Youku:{1} jellyfin: {2}", title, pubYear, itemPubYear); + log.LogDebug("[{0}] 发行年份不一致,忽略处理. Youku:{1} jellyfin: {2}", title, pubYear, itemPubYear); continue; }