Files
jellyfin-plugin-danmu/Jellyfin.Plugin.Danmu/LibraryManagerEventsHelper.cs
2025-10-29 21:45:26 +08:00

1006 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.Controller.Persistence;
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<LibraryEvent> _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 IItemRepository _itemRepository;
private readonly ILibraryManager _libraryManager;
private readonly ILogger<LibraryManagerEventsHelper> _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();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManagerEventsHelper"/> class.
/// </summary>
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="api">The <see cref="BilibiliApi"/>.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public LibraryManagerEventsHelper(IItemRepository itemRepository, ILibraryManager libraryManager, ILoggerFactory loggerFactory, Jellyfin.Plugin.Danmu.Core.IFileSystem fileSystem, ScraperManager scraperManager)
{
_queuedEvents = new List<LibraryEvent>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());
_itemRepository = itemRepository;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<LibraryManagerEventsHelper>();
_fileSystem = fileSystem;
_scraperManager = scraperManager;
}
/// <summary>
/// Queues an item to be added to trakt.
/// </summary>
/// <param name="item"> The <see cref="BaseItem"/>.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
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 });
}
}
/// <summary>
/// Wait for timer callback to be completed.
/// </summary>
private async void OnQueueTimerCallback(object state)
{
try
{
await OnQueueTimerCallbackInternal().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in OnQueueTimerCallbackInternal");
}
}
/// <summary>
/// Wait for timer to be completed.
/// </summary>
private async Task OnQueueTimerCallbackInternal()
{
// _logger.LogInformation("Timer elapsed - processing queued items");
List<LibraryEvent> queue;
lock (_queuedEvents)
{
if (!_queuedEvents.Any())
{
_logger.LogInformation("No events... stopping queue timer");
return;
}
queue = _queuedEvents.ToList();
_queuedEvents.Clear();
}
var queuedMovieAdds = new List<LibraryEvent>();
var queuedMovieUpdates = new List<LibraryEvent>();
var queuedMovieForces = new List<LibraryEvent>();
var queuedEpisodeAdds = new List<LibraryEvent>();
var queuedEpisodeUpdates = new List<LibraryEvent>();
var queuedEpisodeForces = new List<LibraryEvent>();
var queuedShowAdds = new List<LibraryEvent>();
var queuedShowUpdates = new List<LibraryEvent>();
var queuedSeasonAdds = new List<LibraryEvent>();
var queuedSeasonUpdates = new List<LibraryEvent>();
// 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<LibraryEvent>(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<LibraryEvent>(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<LibraryEvent>(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<LibraryEvent>(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<LibraryEvent>(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<LibraryEvent>(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;
}
/// <summary>
/// Processes queued movie events.
/// </summary>
/// <param name="events">The <see cref="LibraryEvent"/> enumerable.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <returns>Task.</returns>
public async Task ProcessQueuedMovieEvents(IReadOnlyCollection<LibraryEvent> 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<BaseItem>();
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);
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);
}
}
}
}
}
/// <summary>
/// Processes queued show events.
/// </summary>
/// <param name="events">The <see cref="LibraryEvent"/> enumerable.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <returns>Task.</returns>
public async Task ProcessQueuedShowEvents(IReadOnlyCollection<LibraryEvent> 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");
}
}
/// <summary>
/// Processes queued season events.
/// </summary>
/// <param name="events">The <see cref="LibraryEvent"/> enumerable.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <returns>Task.</returns>
public async Task ProcessQueuedSeasonEvents(IReadOnlyCollection<LibraryEvent> 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<BaseItem>();
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<BaseItem>();
// 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);
}
}
}
/// <summary>
/// Processes queued episode events.
/// </summary>
/// <param name="events">The <see cref="LibraryEvent"/> enumerable.</param>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <returns>Task.</returns>
public async Task ProcessQueuedEpisodeEvents(IReadOnlyCollection<LibraryEvent> 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<BaseItem>();
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<BaseItem> 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<BaseItem> 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;
}
_itemRepository.SaveItems(new[] { item }, CancellationToken.None);
}
}
_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;
_itemRepository.SaveItems(new[] { item }, CancellationToken.None);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_queueTimer?.Dispose();
}
}
}