using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.Danmu.Core; using Jellyfin.Plugin.Danmu.Model; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Caching.Memory; using Jellyfin.Plugin.Danmu.Scrapers; using Jellyfin.Plugin.Danmu.Core.Extensions; using Jellyfin.Plugin.Danmu.Configuration; namespace Jellyfin.Plugin.Danmu; public class LibraryManagerEventsHelper : IDisposable { private readonly List _queuedEvents; private readonly IMemoryCache _memoryCache; private readonly MemoryCacheEntryOptions _pendingAddExpiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; private readonly MemoryCacheEntryOptions _danmuUpdatedExpiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(24*60) }; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly Jellyfin.Plugin.Danmu.Core.IFileSystem _fileSystem; private Timer _queueTimer; private readonly ScraperManager _scraperManager; public PluginConfiguration Config { get { return Plugin.Instance?.Configuration ?? new Configuration.PluginConfiguration(); } } /// /// Initializes a new instance of the class. /// /// The . /// The . /// The . /// Instance of the interface. public LibraryManagerEventsHelper(ILibraryManager libraryManager, ILoggerFactory loggerFactory, Jellyfin.Plugin.Danmu.Core.IFileSystem fileSystem, ScraperManager scraperManager) { _queuedEvents = new List(); _memoryCache = new MemoryCache(new MemoryCacheOptions()); _libraryManager = libraryManager; _logger = loggerFactory.CreateLogger(); _fileSystem = fileSystem; _scraperManager = scraperManager; } /// /// Queues an item to be added to trakt. /// /// The . /// The . public void QueueItem(BaseItem item, EventType eventType) { lock (_queuedEvents) { if (item == null) { throw new ArgumentNullException(nameof(item)); } if (_queueTimer == null) { _queueTimer = new Timer( OnQueueTimerCallback, null, TimeSpan.FromMilliseconds(10000), Timeout.InfiniteTimeSpan); } else { _queueTimer.Change(TimeSpan.FromMilliseconds(10000), Timeout.InfiniteTimeSpan); } _queuedEvents.Add(new LibraryEvent { Item = item, EventType = eventType }); } } /// /// Wait for timer callback to be completed. /// private async void OnQueueTimerCallback(object state) { try { await OnQueueTimerCallbackInternal().ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "Error in OnQueueTimerCallbackInternal"); } } /// /// Wait for timer to be completed. /// private async Task OnQueueTimerCallbackInternal() { // _logger.LogInformation("Timer elapsed - processing queued items"); List queue; lock (_queuedEvents) { if (!_queuedEvents.Any()) { _logger.LogInformation("No events... stopping queue timer"); return; } queue = _queuedEvents.ToList(); _queuedEvents.Clear(); } var queuedMovieAdds = new List(); var queuedMovieUpdates = new List(); var queuedMovieForces = new List(); var queuedEpisodeAdds = new List(); var queuedEpisodeUpdates = new List(); var queuedEpisodeForces = new List(); var queuedShowAdds = new List(); var queuedShowUpdates = new List(); var queuedSeasonAdds = new List(); var queuedSeasonUpdates = new List(); // add事件可能会在获取元数据完之前执行,导致可能会中断元数据获取,通过pending集合把add事件延缓到获取元数据后再执行(获取完元数据后,一般会多推送一个update事件) foreach (var ev in queue) { // item所在的媒体库不启用弹幕插件,忽略处理 if (IsIgnoreItem(ev.Item)) { continue; } switch (ev.Item) { case Movie when ev.EventType is EventType.Add: _logger.LogInformation("Movie add: {0}", ev.Item.Name); _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 (_memoryCache.TryGetValue(ev.Item.Id, out LibraryEvent addMovieEv)) { queuedMovieAdds.Add(addMovieEv); _memoryCache.Remove(ev.Item.Id); } else { queuedMovieUpdates.Add(ev); } break; case Movie when ev.EventType is EventType.Force: _logger.LogInformation("Movie force: {0}", ev.Item.Name); queuedMovieForces.Add(ev); break; case Series when ev.EventType is EventType.Add: _logger.LogInformation("Series add: {0}", ev.Item.Name); // _pendingAddEventCache.Set(ev.Item.Id, ev, _expiredOption); break; case Series when ev.EventType is EventType.Update: _logger.LogInformation("Series update: {0}", ev.Item.Name); // if (_pendingAddEventCache.TryGetValue(ev.Item.Id, out LibraryEvent addSerieEv)) // { // // 紧跟add事件的update事件不需要处理 // _pendingAddEventCache.Remove(ev.Item.Id); // } // else // { // queuedShowUpdates.Add(ev); // } break; case Season when ev.EventType is EventType.Add: _logger.LogInformation("Season add: {0}", ev.Item.Name); _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 (_memoryCache.TryGetValue(ev.Item.Id, out LibraryEvent addSeasonEv)) { queuedSeasonAdds.Add(addSeasonEv); _memoryCache.Remove(ev.Item.Id); } else { queuedSeasonUpdates.Add(ev); } break; case Episode when ev.EventType is EventType.Update: _logger.LogInformation("Episode update: {0}.{1}", ev.Item.IndexNumber, ev.Item.Name); queuedEpisodeUpdates.Add(ev); break; case Episode when ev.EventType is EventType.Force: _logger.LogInformation("Episode force: {0}.{1}", ev.Item.IndexNumber, ev.Item.Name); queuedEpisodeForces.Add(ev); break; } } // 对于剧集,处理顺序也很重要(Add事件后,会刷新元数据,导致会同时推送Update事件) await ProcessQueuedMovieEvents(queuedMovieAdds, EventType.Add).ConfigureAwait(false); await ProcessQueuedMovieEvents(queuedMovieUpdates, EventType.Update).ConfigureAwait(false); await ProcessQueuedShowEvents(queuedShowAdds, EventType.Add).ConfigureAwait(false); await ProcessQueuedSeasonEvents(queuedSeasonAdds, EventType.Add).ConfigureAwait(false); await ProcessQueuedEpisodeEvents(queuedEpisodeAdds, EventType.Add).ConfigureAwait(false); await ProcessQueuedShowEvents(queuedShowUpdates, EventType.Update).ConfigureAwait(false); await ProcessQueuedSeasonEvents(queuedSeasonUpdates, EventType.Update).ConfigureAwait(false); await ProcessQueuedEpisodeEvents(queuedEpisodeUpdates, EventType.Update).ConfigureAwait(false); await ProcessQueuedMovieEvents(queuedMovieForces, EventType.Force).ConfigureAwait(false); await ProcessQueuedEpisodeEvents(queuedEpisodeForces, EventType.Force).ConfigureAwait(false); } public bool IsIgnoreItem(BaseItem item) { // item所在的媒体库不启用弹幕插件,忽略处理 var libraryOptions = _libraryManager.GetLibraryOptions(item); if (libraryOptions != null && libraryOptions.DisabledSubtitleFetchers.Contains(Plugin.Instance?.Name)) { this._logger.LogInformation($"媒体库已关闭danmu插件, 忽略处理[{item.Name}]."); return true; } return false; } /// /// Processes queued movie events. /// /// The enumerable. /// The . /// Task. public async Task ProcessQueuedMovieEvents(IReadOnlyCollection events, EventType eventType) { if (events.Count == 0) { return; } _logger.LogDebug("Processing {Count} movies with event type {EventType}", events.Count, eventType); var movies = events.Select(lev => (Movie)lev.Item) .Where(lev => !string.IsNullOrEmpty(lev.Name)) .ToHashSet(); // 新增事件也会触发update,不需要处理Add // 更新,判断是否有bvid,有的话刷新弹幕文件 if (eventType == EventType.Add) { var queueUpdateMeta = new List(); foreach (var item in movies) { foreach (var scraper in _scraperManager.All()) { try { // 读取最新数据,要不然取不到年份信息 var currentItem = _libraryManager.GetItemById(item.Id) ?? item; var mediaId = await scraper.SearchMediaId(currentItem).ConfigureAwait(false); if (string.IsNullOrEmpty(mediaId)) { _logger.LogInformation("[{0}]元数据匹配失败:{1} ({2}),尝试文件匹配", scraper.Name, item.Name, item.ProductionYear); mediaId = await scraper.SearchMediaIdByFile((Movie)currentItem).ConfigureAwait(false); if (string.IsNullOrEmpty(mediaId)) { _logger.LogInformation("[{0}]文件匹配失败:{1}", scraper.Name, currentItem.Path); continue; } } var media = await scraper.GetMedia(item, mediaId); if (media != null) { var providerVal = media.Id; var commentId = media.CommentId; _logger.LogInformation("[{0}]匹配成功:name={1} ProviderId: {2}", scraper.Name, item.Name, providerVal); // 更新epid元数据 item.SetProviderId(scraper.ProviderId, providerVal); queueUpdateMeta.Add(item); // 下载弹幕 await this.DownloadDanmu(scraper, item, commentId).ConfigureAwait(false); break; } } catch (FrequentlyRequestException ex) { _logger.LogError(ex, "[{0}]api接口触发风控,中止执行,请稍候再试.", scraper.Name); } catch (Exception ex) { _logger.LogError(ex, "[{0}]Exception handled processing movie events", scraper.Name); } } } await ProcessQueuedUpdateMeta(queueUpdateMeta).ConfigureAwait(false); } // 更新 if (eventType == EventType.Update) { foreach (var item in movies) { foreach (var scraper in _scraperManager.All()) { try { var providerVal = item.GetProviderId(scraper.ProviderId); if (!string.IsNullOrEmpty(providerVal)) { var episode = await scraper.GetMediaEpisode(item, providerVal); if (episode != null) { // 下载弹幕xml文件 await this.DownloadDanmu(scraper, item, episode.CommentId).ConfigureAwait(false); } // TODO:兼容支持用户设置seasonId??? break; } } catch (FrequentlyRequestException ex) { _logger.LogError(ex, "api接口触发风控,中止执行,请稍候再试."); } catch (Exception ex) { _logger.LogError(ex, "Exception handled processing queued movie events"); } } } } // 强制刷新指定来源弹幕 if (eventType == EventType.Force) { foreach (var queueItem in movies) { // 找到选择的scraper var scraper = _scraperManager.All().FirstOrDefault(x => queueItem.ProviderIds.ContainsKey(x.ProviderId)); if (scraper == null) { continue; } // 获取选择的弹幕Id var mediaId = queueItem.GetProviderId(scraper.ProviderId); if (string.IsNullOrEmpty(mediaId)) { continue; } // 获取最新的item数据 var item = _libraryManager.GetItemById(queueItem.Id); var media = await scraper.GetMedia(item, mediaId); if (media != null) { await this.ForceSaveProviderId(item, scraper.ProviderId, media.Id); var episode = await scraper.GetMediaEpisode(item, media.Id); if (episode != null) { // 下载弹幕xml文件 await this.DownloadDanmu(scraper, item, episode.CommentId, true).ConfigureAwait(false); } } } } } /// /// Processes queued show events. /// /// The enumerable. /// The . /// Task. public async Task ProcessQueuedShowEvents(IReadOnlyCollection events, EventType eventType) { if (events.Count == 0) { return; } _logger.LogDebug("Processing {Count} shows with event type {EventType}", events.Count, eventType); var series = events.Select(lev => (Series)lev.Item) .Where(lev => !string.IsNullOrEmpty(lev.Name)) .ToHashSet(); try { if (eventType == EventType.Update) { foreach (var item in series) { var seasons = item.GetSeasons(null, new DtoOptions(false)); foreach (var season in seasons) { // 发现season保存元数据,不会推送update事件,这里通过series的update事件推送刷新 QueueItem(season, eventType); } } } } catch (Exception ex) { _logger.LogError(ex, "Exception handled processing queued show events"); } } /// /// Processes queued season events. /// /// The enumerable. /// The . /// Task. public async Task ProcessQueuedSeasonEvents(IReadOnlyCollection events, EventType eventType) { if (events.Count == 0) { return; } _logger.LogDebug("Processing {Count} seasons with event type {EventType}", events.Count, eventType); var seasons = events.Select(lev => (Season)lev.Item) .Where(lev => !string.IsNullOrEmpty(lev.Name)) .ToHashSet(); if (eventType == EventType.Add) { 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("special特典文件夹不处理:name={0} number={1}", season.Name, season.IndexNumber); continue; } var series = season.GetParent(); foreach (var scraper in _scraperManager.All()) { try { // 读取最新数据,要不然取不到年份信息(不能对GetItemById的对象直接修改属性,要不然会直接改到数据!!!!) var currentItem = _libraryManager.GetItemById(season.Id); if (currentItem != null) { season.ProductionYear = currentItem.ProductionYear; } // 季的名称不准确,改使用series的名称 if (series != null) { season.Name = series.Name; } var mediaId = await scraper.SearchMediaId(season); if (string.IsNullOrEmpty(mediaId)) { _logger.LogInformation("[{0}]匹配失败:{1} ({2})", scraper.Name, season.Name, season.ProductionYear); continue; } var media = await scraper.GetMedia(season, mediaId); if (media == null) { _logger.LogInformation("[{0}]匹配成功,但获取不到视频信息. id: {1}", scraper.Name, mediaId); continue; } // 更新seasonId元数据 season.SetProviderId(scraper.ProviderId, mediaId); queueUpdateMeta.Add(season); _logger.LogInformation("[{0}]匹配成功:name={1} season_number={2} ProviderId: {3}", scraper.Name, season.Name, season.IndexNumber, mediaId); break; } catch (FrequentlyRequestException ex) { _logger.LogError(ex, "api接口触发风控,中止执行,请稍候再试."); } catch (Exception ex) { _logger.LogError(ex, "Exception handled processing season events"); } } } // 保存元数据 await ProcessQueuedUpdateMeta(queueUpdateMeta).ConfigureAwait(false); } if (eventType == EventType.Update) { foreach (var season in seasons) { // // 虚拟季第一次请求忽略 // if (season.LocationType == LocationType.Virtual && season.IndexNumber is null) // { // continue; // } var queueUpdateMeta = new List(); // GetEpisodes一定要取所有fields,要不然更新会导致重建虚拟season季信息 // TODO:可能出现未刮削完,就触发获取弹幕,导致GetEpisodes只能获取到部分剧集的情况 var episodes = this.GetExistingEpisodes(season); if (episodes.Count == 0) { continue; } foreach (var scraper in _scraperManager.All()) { try { var providerVal = season.GetProviderId(scraper.ProviderId); if (string.IsNullOrEmpty(providerVal)) { continue; } var media = await scraper.GetMedia(season, providerVal); if (media == null) { _logger.LogInformation("[{0}]获取不到视频信息. ProviderId: {1}", scraper.Name, providerVal); break; } 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, fileName); continue; } if (indexNumber > media.Episodes.Count) { _logger.LogInformation("[{0}]匹配失败,集号超过总集数,可能识别集号错误. [{1}]{2} indexNumber: {3}", scraper.Name, season.Name, fileName, indexNumber); continue; } if (this.Config.DownloadOption.EnableEpisodeCountSame && media.Episodes.Count != episodes.Count) { _logger.LogInformation("[{0}]刷新弹幕失败, 集数不一致。video: {1}.{2} 弹幕数:{3} 集数:{4}", scraper.Name, indexNumber, episode.Name, media.Episodes.Count, episodes.Count); continue; } var epId = media.Episodes[idx].Id; var commentId = media.Episodes[idx].CommentId; _logger.LogInformation("[{0}]成功匹配. {1}.{2} -> epId: {3} cid: {4}", scraper.Name, indexNumber, episode.Name, epId, commentId); // 更新eposide元数据 var episodeProviderVal = episode.GetProviderId(scraper.ProviderId); if (!string.IsNullOrEmpty(epId) && episodeProviderVal != epId) { episode.SetProviderId(scraper.ProviderId, epId); queueUpdateMeta.Add(episode); } // 下载弹幕 await this.DownloadDanmu(scraper, episode, commentId).ConfigureAwait(false); } break; } catch (FrequentlyRequestException ex) { _logger.LogError(ex, "api接口触发风控,中止执行,请稍候再试."); } catch (Exception ex) { _logger.LogError(ex, "Exception handled processing queued movie events"); } } // 保存元数据 await ProcessQueuedUpdateMeta(queueUpdateMeta).ConfigureAwait(false); } } } /// /// Processes queued episode events. /// /// The enumerable. /// The . /// Task. public async Task ProcessQueuedEpisodeEvents(IReadOnlyCollection events, EventType eventType) { if (events.Count == 0) { return; } _logger.LogDebug("Processing {Count} episodes with event type {EventType}", events.Count, eventType); var items = events.Select(lev => (Episode)lev.Item) .Where(lev => !string.IsNullOrEmpty(lev.Name)) .ToHashSet(); // 判断epid,有的话刷新弹幕文件 if (eventType == EventType.Update) { var queueUpdateMeta = new List(); foreach (var item in items) { // 如果 Episode 没有弹幕元数据,表示该集是刮削完成后再新增的,需要重新匹配获取 var scrapers = this._scraperManager.All(); var season = item.Season; var allDanmuProviderIds = scrapers.Select(x => x.ProviderId).ToList(); var episodeFirstProviderId = allDanmuProviderIds.FirstOrDefault(x => !string.IsNullOrEmpty(item.GetProviderId(x))); var seasonFirstProviderId = allDanmuProviderIds.FirstOrDefault(x => !string.IsNullOrEmpty(season.GetProviderId(x))); if (string.IsNullOrEmpty(episodeFirstProviderId) && item.IndexNumber.HasValue) { AbstractScraper? scraper = null; string? providerVal = null; // 如果 Season 没有弹幕元数据,说明 Add 时使用元数据搜索失败了,此时使用文件信息再次匹配 if (string.IsNullOrEmpty(seasonFirstProviderId)) { foreach (var s in scrapers) { var mediaId = await s.SearchMediaIdByFile(item).ConfigureAwait(false); if (!string.IsNullOrEmpty(mediaId)) { scraper = s; providerVal = mediaId; break; } } } else { scraper = scrapers.First(x => x.ProviderId == seasonFirstProviderId); providerVal = season.GetProviderId(seasonFirstProviderId); } if (scraper == null) { _logger.LogInformation("使用文件匹配失败:{0}", item.Path); continue; } var media = await scraper.GetMedia(season, providerVal); if (media != null) { var fileName = Path.GetFileName(item.Path); var indexNumber = item.IndexNumber ?? 0; if (indexNumber <= 0) { this._logger.LogInformation("[{0}]匹配失败,缺少集号. [{1}]{2}", scraper.Name, season.Name, fileName); continue; } if (indexNumber > media.Episodes.Count) { this._logger.LogInformation("[{0}]匹配失败,集号超过总集数,可能识别集号错误. [{1}]{2} indexNumber: {3}", scraper.Name, season.Name, fileName, indexNumber); continue; } var episodes = this.GetExistingEpisodes(season); if (this.Config.DownloadOption.EnableEpisodeCountSame && media.Episodes.Count != episodes.Count) { this._logger.LogInformation("[{0}]刷新弹幕失败, 集数不一致。video: {1}.{2} 弹幕数:{3} 集数:{4}", scraper.Name, indexNumber, item.Name, media.Episodes.Count, episodes.Count); continue; } var idx = indexNumber - 1; var epId = media.Episodes[idx].Id; var commentId = media.Episodes[idx].CommentId; this._logger.LogInformation("[{0}]成功匹配. {1}.{2} -> epId: {3} cid: {4}", scraper.Name, item.IndexNumber, item.Name, epId, commentId); // 更新 eposide 元数据 var episodeProviderVal = item.GetProviderId(scraper.ProviderId); if (!string.IsNullOrEmpty(epId) && episodeProviderVal != epId) { item.SetProviderId(scraper.ProviderId, epId); queueUpdateMeta.Add(item); } // 下载弹幕 await this.DownloadDanmu(scraper, item, commentId).ConfigureAwait(false); continue; } } // 刷新弹幕 foreach (var scraper in _scraperManager.All()) { try { var providerVal = item.GetProviderId(scraper.ProviderId); if (string.IsNullOrEmpty(providerVal)) { continue; } var episode = await scraper.GetMediaEpisode(item, providerVal); if (episode != null) { // 下载弹幕xml文件 await this.DownloadDanmu(scraper, item, episode.CommentId).ConfigureAwait(false); } break; } catch (FrequentlyRequestException ex) { _logger.LogError(ex, "api接口触发风控,中止执行,请稍候再试."); } catch (Exception ex) { _logger.LogError(ex, "Exception handled processing queued movie events"); } } } // 保存元数据 await ProcessQueuedUpdateMeta(queueUpdateMeta).ConfigureAwait(false); } // 强制刷新指定来源弹幕(手动搜索强刷忽略集数不一致处理) if (eventType == EventType.Force) { foreach (var queueItem in items) { // 找到选择的scraper var scraper = _scraperManager.All().FirstOrDefault(x => queueItem.ProviderIds.ContainsKey(x.ProviderId)); if (scraper == null) { continue; } // 获取选择的弹幕Id var mediaId = queueItem.GetProviderId(scraper.ProviderId); if (string.IsNullOrEmpty(mediaId)) { continue; } // 获取最新的item数据 var item = _libraryManager.GetItemById(queueItem.Id); var season = ((Episode)item).Season; if (season == null) { continue; } var media = await scraper.GetMedia(season, mediaId); if (media != null) { // 更新季元数据 await ForceSaveProviderId(season, scraper.ProviderId, media.Id); // 更新所有剧集元数据,GetEpisodes一定要取所有fields,要不然更新会导致重建虚拟season季信息 var episodeList = season.GetEpisodes(); 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; } var epId = media.Episodes[indexNumber - 1].Id; var commentId = media.Episodes[indexNumber - 1].CommentId; // 下载弹幕xml文件 await this.DownloadDanmu(scraper, episode, commentId, true).ConfigureAwait(false); // 更新剧集元数据 await ForceSaveProviderId(episode, scraper.ProviderId, epId); } } } } } private List GetExistingEpisodes(Season season) { var episodes = season.GetEpisodes() .Where(i => !i.IsVirtualItem) .ToList(); // 不处理季文件夹下的特典和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; } return episodes; } // 调用UpdateToRepositoryAsync后,但未完成时,会导致GetEpisodes返回缺少正在处理的集数,所以采用统一最后处理 private async Task ProcessQueuedUpdateMeta(List queue) { if (queue == null || queue.Count <= 0) { return; } foreach (var queueItem in queue) { // 获取最新的item数据 var item = _libraryManager.GetItemById(queueItem.Id); if (item != null) { // 合并新添加的provider id foreach (var pair in queueItem.ProviderIds) { if (string.IsNullOrEmpty(pair.Value)) { continue; } item.ProviderIds[pair.Key] = pair.Value; } await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } } _logger.LogInformation("更新epid到元数据完成。item数:{0}", queue.Count); } public async Task DownloadDanmu(AbstractScraper scraper, BaseItem item, string commentId, bool ignoreCheck = false) { // 下载弹幕xml文件 var checkDownloadedKey = $"{item.Id}_{commentId}"; try { // 弹幕24小时内更新过,忽略处理(有时Update事件会重复执行) if (!ignoreCheck && _memoryCache.TryGetValue(checkDownloadedKey, out var latestDownloaded)) { _logger.LogInformation("[{0}]最近24小时已更新过弹幕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) { var bytes = danmaku.ToXml(); if (bytes.Length < 1024) { _logger.LogInformation("[{0}]弹幕内容少于1KB,忽略处理:{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 ?? 1, item.Name, commentId); } else { _memoryCache.Remove(checkDownloadedKey); } } catch (Exception ex) { _memoryCache.Remove(checkDownloadedKey); _logger.LogError(ex, "[{0}]Exception handled download danmu file. name={1}", scraper.Name, item.Name); } } 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)) { return false; } var lastWriteTime = this._fileSystem.GetLastWriteTime(danmuPath); var diff = DateTime.Now - lastWriteTime; return diff.TotalSeconds < 300; } private async Task SaveDanmu(BaseItem item, byte[] bytes) { // 单元测试时为null if (item.FileNameWithoutExtension == null) return; // 下载弹幕xml文件 var danmuPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".xml"); await this._fileSystem.WriteAllBytesAsync(danmuPath, bytes, CancellationToken.None).ConfigureAwait(false); if (this.Config.ToAss && bytes.Length > 0) { var assConfig = new Danmaku2Ass.Config(); assConfig.Title = item.Name; if (!string.IsNullOrEmpty(this.Config.AssFont.Trim())) { assConfig.FontName = this.Config.AssFont; } if (!string.IsNullOrEmpty(this.Config.AssFontSize.Trim())) { assConfig.BaseFontSize = this.Config.AssFontSize.Trim().ToInt(); } if (!string.IsNullOrEmpty(this.Config.AssTextOpacity.Trim())) { assConfig.TextOpacity = this.Config.AssTextOpacity.Trim().ToFloat(); } if (!string.IsNullOrEmpty(this.Config.AssLineCount.Trim())) { assConfig.LineCount = this.Config.AssLineCount.Trim().ToInt(); } if (!string.IsNullOrEmpty(this.Config.AssSpeed.Trim())) { assConfig.TuneDuration = this.Config.AssSpeed.Trim().ToInt() - 8; } if (this.Config.AssRemoveEmoji) { Danmaku2Ass.Bilibili.GetInstance().SetCustomFilter(true); } var assPath = Path.Combine(item.ContainingFolderPath, item.FileNameWithoutExtension + ".danmu.ass"); Danmaku2Ass.Bilibili.GetInstance().Create(bytes, assConfig, assPath); } } private async Task ForceSaveProviderId(BaseItem item, string providerId, string providerVal) { // 先清空旧弹幕的所有元数据 foreach (var s in _scraperManager.All()) { item.ProviderIds.Remove(s.ProviderId); } // 保存指定弹幕元数据 item.ProviderIds[providerId] = providerVal; await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { _queueTimer?.Dispose(); } } }