refactor: optimize api cache

This commit is contained in:
cxfksword
2025-11-19 11:10:02 +08:00
parent ab5ead5d62
commit 2265480e30
13 changed files with 428 additions and 121 deletions

View File

@@ -5,6 +5,8 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Plugin.Danmu.Controllers;
using Jellyfin.Plugin.Danmu.Controllers.Entity;
using Jellyfin.Plugin.Danmu.Core;
using Jellyfin.Plugin.Danmu.Scrapers;
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
using Jellyfin.Plugin.Danmu.Scrapers.Dandan;
@@ -12,10 +14,10 @@ using Jellyfin.Plugin.Danmu.Scrapers.Iqiyi;
using Jellyfin.Plugin.Danmu.Scrapers.Tencent;
using Jellyfin.Plugin.Danmu.Scrapers.Mgtv;
using Jellyfin.Plugin.Danmu.Scrapers.Youku;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.IO;
using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging;
using Moq;
using System.IO;
namespace Jellyfin.Plugin.Danmu.Test
{
@@ -39,26 +41,19 @@ namespace Jellyfin.Plugin.Danmu.Test
// _scraperManager.Register(new Tencent(loggerFactory));
// _scraperManager.Register(new Mgtv(loggerFactory));
// Mock 依赖
var fileSystemMock = new Mock<IFileSystem>();
var libraryManagerMock = new Mock<ILibraryManager>();
var itemRepositoryMock = new Mock<MediaBrowser.Controller.Persistence.IItemRepository>();
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(
itemRepositoryMock.Object,
libraryManagerMock.Object,
loggerFactory,
fileSystemStub.Object,
_scraperManager);
// 配置插件存储路径,确保文件缓存可用
var pluginConfigPath = Path.Combine(Path.GetTempPath(), "danmu-plugin-tests");
Directory.CreateDirectory(pluginConfigPath);
var applicationPathsMock = new Mock<IApplicationPaths>();
applicationPathsMock.SetupGet(p => p.PluginConfigurationsPath).Returns(pluginConfigPath);
var fileCache = new FileCache<AnimeCacheItem>(applicationPathsMock.Object, loggerFactory, TimeSpan.FromDays(31), TimeSpan.FromSeconds(60));
// 创建 ApiController 实例
_apiController = new ApiController(
fileSystemMock.Object,
loggerFactory,
libraryManagerEventsHelper,
libraryManagerMock.Object,
_scraperManager);
_scraperManager,
fileCache);
}
[TestMethod]

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Globalization;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
@@ -26,11 +27,8 @@ namespace Jellyfin.Plugin.Danmu.Controllers
[AllowAnonymous]
public class ApiController : ControllerBase
{
private readonly ILibraryManager _libraryManager;
private readonly LibraryManagerEventsHelper _libraryManagerEventsHelper;
private readonly MediaBrowser.Model.IO.IFileSystem _fileSystem;
private readonly ScraperManager _scraperManager;
private static TimeSpan _cacheExpiration = TimeSpan.FromDays(31);
private static TimeSpan _cacheExpiration = TimeSpan.FromDays(1);
private static readonly IMemoryCache _animeIdCache = new MemoryCache(new MemoryCacheOptions
{
// 设置缓存大小限制(可选)
@@ -42,65 +40,26 @@ namespace Jellyfin.Plugin.Danmu.Controllers
SizeLimit = 10000
});
private readonly FileCache<AnimeCacheItem> _animeFileCache;
private readonly ILogger<ApiController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ApiController"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="MediaBrowser.Model.IO.IFileSystem"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="libraryManagerEventsHelper">Instance of the <see cref="LibraryManagerEventsHelper"/> class.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="scraperManager">Instance of the <see cref="ScraperManager"/> class.</param>
/// <param name="animeFileCache">File-backed cache injected via dependency injection.</param>
public ApiController(
MediaBrowser.Model.IO.IFileSystem fileSystem,
ILoggerFactory loggerFactory,
LibraryManagerEventsHelper libraryManagerEventsHelper,
ILibraryManager libraryManager,
ScraperManager scraperManager)
ScraperManager scraperManager,
FileCache<AnimeCacheItem> animeFileCache)
{
_fileSystem = fileSystem;
_logger = loggerFactory.CreateLogger<ApiController>();
_libraryManager = libraryManager;
_libraryManagerEventsHelper = libraryManagerEventsHelper;
_scraperManager = scraperManager;
_animeFileCache = animeFileCache ?? throw new ArgumentNullException(nameof(animeFileCache));
}
/// <summary>
/// 将 scraper ID 转换为带有 HashPrefix 的 11 位 long 类型 AnimeId
/// 格式:前 2 位是 HashPrefix后 9 位是 ID 的哈希值
/// 例如HashPrefix=10, Id="abc123" => 10xxxxxxxxx (后9位是哈希值)
/// </summary>
/// <param name="scraper">弹幕源</param>
/// <param name="id">原始 ID 字符串(可以是任意字母数字组合)</param>
/// <param name="animeData">完整的 Anime 数据(可选)</param>
/// <returns>11 位 long 类型的 AnimeId失败时返回 0</returns>
private long ConvertToHashId(AbstractScraper scraper, string id)
{
if (string.IsNullOrEmpty(id))
{
return 0;
}
long animeId;
// 如果是纯数字且不超过 9 位,直接使用
if (long.TryParse(id, out var numericId) && numericId <= 999999999)
{
animeId = ((long)scraper.HashPrefix * 1000000000) + numericId;
}
else
{
// 对于非纯数字或超长的 ID使用哈希算法转换为 9 位数字
// 使用 GetHashCode 并取绝对值,然后模 999999999 确保在 9 位范围内
var hashCode = id.GetHashCode();
var hashValue = Math.Abs(hashCode) % 1000000000; // 确保在 0-999999999 范围内
animeId = ((long)scraper.HashPrefix * 1000000000) + hashValue;
}
return animeId;
}
/// <summary>
/// 查找弹幕
@@ -282,14 +241,30 @@ namespace Jellyfin.Plugin.Danmu.Controllers
// 从缓存中获取Anime数据
if (!_animeIdCache.TryGetValue(animeId, out AnimeCacheItem? animeCacheItem) || animeCacheItem == null)
{
return new ApiResult<Anime>()
if (this._animeFileCache.TryGetValue(animeId.ToString(CultureInfo.InvariantCulture), out animeCacheItem) && animeCacheItem != null)
{
ErrorCode = 404,
Success = false,
ErrorMessage = "Anime not found",
};
// 将数据重新放入内存缓存
var cacheOptions = new MemoryCacheEntryOptions
{
Size = 1,
AbsoluteExpirationRelativeToNow = _cacheExpiration,
};
_animeIdCache.Set(animeId, animeCacheItem, cacheOptions);
}
else
{
return new ApiResult<Anime>()
{
ErrorCode = 404,
Success = false,
ErrorMessage = "Anime not found",
};
}
}
// 延长文件缓存时间或写入文件缓存
this._animeFileCache.Set(animeId.ToString(CultureInfo.InvariantCulture), animeCacheItem);
var animeData = animeCacheItem.AnimeData;
if (animeData == null)
{
@@ -396,6 +371,14 @@ namespace Jellyfin.Plugin.Danmu.Controllers
{
throw new ResourceNotFoundException();
}
// 延长缓存时间
var cacheOptions = new MemoryCacheEntryOptions
{
Size = 1,
AbsoluteExpirationRelativeToNow = _cacheExpiration,
};
_episodeIdCache.Set(episodeId, commentCacheItem, cacheOptions);
var scraper = this._scraperManager.All().FirstOrDefault(s => s.ProviderId == commentCacheItem.ScraperProviderId);
if (scraper == null)
@@ -422,7 +405,6 @@ namespace Jellyfin.Plugin.Danmu.Controllers
Cid = item.Id,
P = $"{item.Progress / 1000.0:F2},{item.Mode},{item.Color},{item.MidHash}",
Text = item.Content,
Time = (uint)(item.Progress / 1000),
}).ToList()
};
@@ -433,5 +415,42 @@ namespace Jellyfin.Plugin.Danmu.Controllers
throw new ResourceNotFoundException();
}
/// <summary>
/// 将 scraper ID 转换为带有 HashPrefix 的 11 位 long 类型 AnimeId
/// 格式:前 2 位是 HashPrefix后 9 位是 ID 的哈希值
/// 例如HashPrefix=10, Id="abc123" => 10xxxxxxxxx (后9位是哈希值)
/// </summary>
/// <param name="scraper">弹幕源</param>
/// <param name="id">原始 ID 字符串(可以是任意字母数字组合)</param>
/// <param name="animeData">完整的 Anime 数据(可选)</param>
/// <returns>11 位 long 类型的 AnimeId失败时返回 0</returns>
private long ConvertToHashId(AbstractScraper scraper, string id)
{
if (string.IsNullOrEmpty(id))
{
return 0;
}
long animeId;
// 如果是纯数字且不超过 9 位,直接使用
if (long.TryParse(id, out var numericId) && numericId <= 999999999)
{
animeId = ((long)scraper.HashPrefix * 1000000000) + numericId;
}
else
{
// 对于非纯数字或超长的 ID使用哈希算法转换为 9 位数字
// 使用 GetHashCode 并取绝对值,然后模 999999999 确保在 9 位范围内
var hashCode = id.GetHashCode();
var hashValue = Math.Abs(hashCode) % 1000000000; // 确保在 0-999999999 范围内
animeId = ((long)scraper.HashPrefix * 1000000000) + hashValue;
}
return animeId;
}
}
}

View File

@@ -1,19 +1,39 @@
namespace Jellyfin.Plugin.Danmu.Controllers.Entity
{
using System.Text.Json.Serialization;
/// <summary>
/// 表示由弹幕源抓取并缓存的番剧条目。
/// </summary>
public class AnimeCacheItem
{
/// <summary>
/// Initializes a new instance of the <see cref="AnimeCacheItem"/> class用于封装弹幕源缓存的番剧条目.
/// </summary>
/// <param name="scraperProviderId">弹幕源提供的唯一 ProviderId。</param>
/// <param name="id">对应弹幕源的番剧标识。</param>
/// <param name="animeData">弹幕源返回的番剧原始数据。</param>
[JsonConstructor]
public AnimeCacheItem(string scraperProviderId, string id, Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity.Anime? animeData)
{
this.ScraperProviderId = scraperProviderId;
this.Id = id;
this.AnimeData = animeData;
}
/// <summary>
/// Gets or sets 弹幕源 ProviderId例如 BilibiliID.
/// </summary>
public string ScraperProviderId { get; set; }
/// <summary>
/// Gets or sets 弹幕源内部使用的番剧编号.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets 弹幕源返回的番剧原始数据对象.
/// </summary>
public Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity.Anime? AnimeData { get; set; }
public AnimeCacheItem(string providerId, string id, Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity.Anime anime)
{
ScraperProviderId = providerId;
Id = id;
AnimeData = anime;
}
}
}

View File

@@ -0,0 +1,268 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Danmu.Core;
/// <summary>
/// Simple thread-safe cache that keeps data in-memory and persists to disk with a delayed flush.
/// </summary>
/// <typeparam name="TValue">Type of cached values.</typeparam>
public sealed class FileCache<TValue> : IDisposable
{
private readonly ConcurrentDictionary<string, CacheEntry> _entries = new(StringComparer.OrdinalIgnoreCase);
private readonly TimeSpan _defaultTtl;
private readonly TimeSpan _saveDelay;
private readonly JsonSerializerOptions _serializerOptions;
private readonly object _flushLock = new();
private readonly string _filePath;
private Timer? _flushTimer;
private bool _disposed;
private readonly ILogger<FileCache<TValue>> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="FileCache{TValue}"/> class.
/// </summary>
/// <param name="applicationPaths">Provides plugin specific storage locations.</param>
/// <param name="defaultTtl">Default time-to-live for entries when not specified.</param>
/// <param name="saveDelay">Delay before pending changes are flushed to disk.</param>
public FileCache(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, TimeSpan? defaultTtl = null, TimeSpan? saveDelay = null)
{
ArgumentNullException.ThrowIfNull(applicationPaths);
_logger = loggerFactory.CreateLogger<FileCache<TValue>>();
_defaultTtl = defaultTtl ?? TimeSpan.FromHours(12);
_saveDelay = saveDelay ?? TimeSpan.FromSeconds(10);
_serializerOptions = new JsonSerializerOptions
{
WriteIndented = false,
PropertyNameCaseInsensitive = true,
};
_filePath = Path.Join(applicationPaths.PluginConfigurationsPath, "Jellyfin.Plugin.Danmu.Cache.dat");
LoadFromDisk();
}
/// <summary>
/// Attempts to retrieve a cached value.
/// </summary>
/// <param name="key">Cache key.</param>
/// <param name="value">Resolved value when found.</param>
/// <returns>True when the value exists and did not expire.</returns>
public bool TryGetValue(string key, out TValue value)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key cannot be null or empty", nameof(key));
}
value = default!;
if (!_entries.TryGetValue(key, out var entry))
{
return false;
}
if (entry.IsExpired)
{
_entries.TryRemove(key, out _);
ScheduleFlush();
return false;
}
value = entry.Value;
return true;
}
/// <summary>
/// Adds or updates a cache item.
/// </summary>
/// <param name="key">Cache key.</param>
/// <param name="value">Value to cache.</param>
/// <param name="ttl">Optional per-entry expiration override.</param>
public void Set(string key, TValue value, TimeSpan? ttl = null)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key cannot be null or empty", nameof(key));
}
var expiration = DateTimeOffset.UtcNow.Add(ttl ?? _defaultTtl);
_entries[key] = new CacheEntry(value, expiration);
ScheduleFlush();
}
/// <summary>
/// Removes a cached entry.
/// </summary>
/// <param name="key">Cache key.</param>
/// <returns>True when an entry was removed.</returns>
public bool Remove(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key cannot be null or empty", nameof(key));
}
var removed = _entries.TryRemove(key, out _);
if (removed)
{
ScheduleFlush();
}
return removed;
}
/// <summary>
/// Clears all cached entries.
/// </summary>
public void Clear()
{
_entries.Clear();
ScheduleFlush();
}
/// <summary>
/// Gets the number of cached entries currently in memory.
/// </summary>
public int Count => _entries.Count;
private void LoadFromDisk()
{
var path = _filePath;
if (string.IsNullOrEmpty(path))
{
return;
}
try
{
if (!File.Exists(path))
{
return;
}
using var stream = File.OpenRead(path);
var cache = JsonSerializer.Deserialize<Dictionary<string, CacheEntry>>(stream, _serializerOptions);
if (cache == null)
{
return;
}
foreach (var pair in cache)
{
if (pair.Value != null && !pair.Value.IsExpired)
{
_entries.TryAdd(pair.Key, pair.Value);
}
}
}
catch (Exception ex)
{
// Log the exception
_logger.LogError(ex, "Failed to load cache from disk");
// Ignore corrupted cache file to keep plugin functional.
_ = ex;
}
}
private void PersistToDisk()
{
if (_disposed || string.IsNullOrEmpty(_filePath))
{
return;
}
RemoveExpiredEntries();
try
{
var directory = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var snapshot = _entries.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase);
using var stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.None);
JsonSerializer.Serialize(stream, snapshot, _serializerOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to persist cache to disk");
// Ignore IO issues; data remains in memory and will retry on next flush.
_ = ex;
}
}
private void RemoveExpiredEntries()
{
var now = DateTimeOffset.UtcNow;
foreach (var pair in _entries)
{
if (pair.Value.Expiration <= now)
{
_entries.TryRemove(pair.Key, out _);
}
}
}
private void ScheduleFlush()
{
if (_disposed || string.IsNullOrEmpty(_filePath))
{
return;
}
lock (_flushLock)
{
_flushTimer ??= new Timer(OnFlushTimer, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
_flushTimer.Change(_saveDelay, Timeout.InfiniteTimeSpan);
}
}
/// <summary>
/// Flushes pending entries to disk and releases resources.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
lock (_flushLock)
{
_flushTimer?.Dispose();
_flushTimer = null;
}
PersistToDisk();
}
private void OnFlushTimer(object? state)
{
PersistToDisk();
}
private sealed class CacheEntry
{
public CacheEntry(TValue value, DateTimeOffset expiration)
{
Value = value;
Expiration = expiration;
}
public TValue Value { get; }
public DateTimeOffset Expiration { get; }
[JsonIgnore]
public bool IsExpired => DateTimeOffset.UtcNow >= Expiration;
}
}

View File

@@ -37,6 +37,7 @@ public abstract class AbstractApi : IDisposable
_cookieContainer = handler.CookieContainer;
httpClient = new HttpClient(handler, true);
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
httpClient.Timeout = TimeSpan.FromSeconds(30);
_memoryCache = new MemoryCache(new MemoryCacheOptions());
}

View File

@@ -59,7 +59,7 @@ public class BilibiliApi : AbstractApi
// 搜索影视
var result = new SearchResult();
var url = $"https://api.bilibili.com/x/web-interface/search/type?keyword={keyword}&search_type=media_ft";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var ftResult = await response.Content.ReadFromJsonAsync<ApiResult<SearchResult>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
if (ftResult != null && ftResult.Code == 0 && ftResult.Data != null)
@@ -69,9 +69,9 @@ public class BilibiliApi : AbstractApi
// 搜索番剧
url = $"https://api.bilibili.com/x/web-interface/search/type?keyword={keyword}&search_type=media_bangumi";
response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var bangumiResult = await response.Content.ReadFromJsonAsync<ApiResult<SearchResult>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
using var bangumiResponse = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
bangumiResponse.EnsureSuccessStatusCode();
var bangumiResult = await bangumiResponse.Content.ReadFromJsonAsync<ApiResult<SearchResult>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
if (bangumiResult != null && bangumiResult.Code == 0 && bangumiResult.Data != null && bangumiResult.Data.Result != null)
{
if (result.Result == null)
@@ -106,7 +106,7 @@ public class BilibiliApi : AbstractApi
// https://api.bilibili.com/x/v1/dm/list.so?oid={cid}
bvid = bvid.Trim();
var pageUrl = $"http://api.bilibili.com/x/player/pagelist?bvid={bvid}";
var response = await this.httpClient.GetAsync(pageUrl, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(pageUrl, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoPart[]>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null && result.Code == 0 && result.Data != null)
@@ -152,7 +152,7 @@ public class BilibiliApi : AbstractApi
}
var url = $"https://api.bilibili.com/x/v1/dm/list.so?oid={cid}";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Request fail. url={url} status_code={response.StatusCode}");
@@ -189,7 +189,7 @@ public class BilibiliApi : AbstractApi
// var url = $"http://api.bilibili.com/pgc/view/web/season?season_id={seasonId}";
var url = $"https://api.bilibili.com/pgc/view/web/ep/list?season_id={seasonId}"; // 接口依赖 referer 过滤正片选集
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoSeason>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null && result.Code == 0 && result.Result != null)
@@ -222,7 +222,7 @@ public class BilibiliApi : AbstractApi
await this.EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
var url = $"https://api.bilibili.com/pgc/view/web/ep/list?ep_id={epId}";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResult<VideoSeason>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null && result.Code == 0 && result.Result != null && result.Result.Episodes != null)
@@ -259,7 +259,7 @@ public class BilibiliApi : AbstractApi
await this.EnsureSessionCookie(cancellationToken).ConfigureAwait(false);
var url = $"https://api.bilibili.com/x/web-interface/view?bvid={bvid}";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResult<Video>>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -290,7 +290,7 @@ public class BilibiliApi : AbstractApi
}
var url = $"https://www.biliplus.com/video/{avid}/";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
@@ -328,7 +328,7 @@ public class BilibiliApi : AbstractApi
while (true)
{
var url = $"https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&pid={aid}&segment_index={segmentIndex}";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.NotModified)
{
// 已经到最后了
@@ -399,7 +399,7 @@ public class BilibiliApi : AbstractApi
return;
}
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}

View File

@@ -69,6 +69,7 @@ public class DandanApi : AbstractApi
: base(loggerFactory.CreateLogger<DandanApi>())
{
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
httpClient.Timeout = TimeSpan.FromSeconds(10);
}
@@ -90,7 +91,7 @@ public class DandanApi : AbstractApi
keyword = HttpUtility.UrlEncode(keyword);
var url = $"https://api.dandanplay.net/api/v2/search/anime?keyword={keyword}";
var response = await this.Request(url, cancellationToken).ConfigureAwait(false);
using var response = await this.Request(url, cancellationToken).ConfigureAwait(false);
var result = await response.Content.ReadFromJsonAsync<SearchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null && result.Success)
{
@@ -131,7 +132,7 @@ public class DandanApi : AbstractApi
}
var url = "https://api.dandanplay.net/api/v2/match";
var response = await this.Request(url, HttpMethod.Post, matchRequest, cancellationToken).ConfigureAwait(false);
using var response = await this.Request(url, HttpMethod.Post, matchRequest, cancellationToken).ConfigureAwait(false);
var result = await response.Content.ReadFromJsonAsync<MatchResponseV2>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null && result.Success && result.Matches != null)
{
@@ -189,7 +190,7 @@ public class DandanApi : AbstractApi
}
var url = $"https://api.dandanplay.net/api/v2/bangumi/{animeId}";
var response = await this.Request(url, cancellationToken).ConfigureAwait(false);
using var response = await this.Request(url, cancellationToken).ConfigureAwait(false);
var result = await response.Content.ReadFromJsonAsync<AnimeResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null && result.Success && result.Bangumi != null)
@@ -224,7 +225,7 @@ public class DandanApi : AbstractApi
var withRelated = this.Config.WithRelatedDanmu ? "true" : "false";
var chConvert = this.Config.ChConvert;
var url = $"https://api.dandanplay.net/api/v2/comment/{epId}?withRelated={withRelated}&chConvert={chConvert}";
var response = await this.Request(url, cancellationToken).ConfigureAwait(false);
using var response = await this.Request(url, cancellationToken).ConfigureAwait(false);
var result = await response.Content.ReadFromJsonAsync<CommentResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (result != null)
{

View File

@@ -38,7 +38,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
public List<Episode>? Episodes { get; set; }
[JsonPropertyName("rating")]
public int Rating { get; set; } = 0;
public float Rating { get; set; } = 0;
[JsonPropertyName("isFavorited")]
public bool IsFavorited { get; set; } = false;

View File

@@ -18,8 +18,5 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Dandan.Entity
[JsonPropertyName("m")]
public string Text { get; set; }
[JsonPropertyName("t")]
public uint Time { get; set; }
}
}

View File

@@ -49,7 +49,7 @@ public class MgtvApi : AbstractApi
keyword = HttpUtility.UrlEncode(keyword);
var url = $"https://mobileso.bz.mgtv.com/msite/search/v2?q={keyword}&pc=30&pn=1&sort=-99&ty=0&du=0&pt=0&corr=1&abroad=0&_support=10000000000000000";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = new List<MgtvSearchItem>();
@@ -100,7 +100,7 @@ public class MgtvApi : AbstractApi
do
{
var url = $"https://pcweb.api.mgtv.com/variety/showlist?allowedRC=1&collection_id={id}&month={month}&page=1&_support=10000000";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<MgtvEpisodeListResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);

View File

@@ -59,7 +59,7 @@ public class TencentApi : AbstractApi
var postData = new TencentSearchRequest() { Query = keyword };
var url = $"https://pbaccess.video.qq.com/trpc.videosearch.mobile_search.HttpMobileRecall/MbSearchHttp";
var response = await httpClient.PostAsJsonAsync<TencentSearchRequest>(url, postData, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.PostAsJsonAsync<TencentSearchRequest>(url, postData, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = new List<TencentVideo>();
@@ -111,7 +111,7 @@ public class TencentApi : AbstractApi
{
var postData = new TencentEpisodeListRequest() { PageParams = new TencentPageParams() { Cid = id, PageSize = $"{pageSize}", PageContext = nextPageContext } };
var url = "https://pbaccess.video.qq.com/trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData?video_appid=3000010&vplatform=2";
var response = await this.httpClient.PostAsJsonAsync<TencentEpisodeListRequest>(url, postData, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.PostAsJsonAsync<TencentEpisodeListRequest>(url, postData, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
nextPageContext = string.Empty;
@@ -175,7 +175,7 @@ public class TencentApi : AbstractApi
}
var url = $"https://dm.video.qq.com/barrage/base/{vid}";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TencentCommentResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -243,7 +243,7 @@ public class TencentApi : AbstractApi
private async Task<List<TencentComment>> GetSegmentDanmuAsync(string vid, string segmentName, CancellationToken cancellationToken)
{
var segmentUrl = $"https://dm.video.qq.com/barrage/segment/{vid}/{segmentName}";
var segmentResponse = await httpClient.GetAsync(segmentUrl, cancellationToken).ConfigureAwait(false);
using var segmentResponse = await httpClient.GetAsync(segmentUrl, cancellationToken).ConfigureAwait(false);
segmentResponse.EnsureSuccessStatusCode();
var segmentResult = await segmentResponse.Content.ReadFromJsonAsync<TencentCommentSegmentResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);

View File

@@ -64,7 +64,7 @@ public class YoukuApi : AbstractApi
keyword = HttpUtility.UrlEncode(keyword);
var ua = HttpUtility.UrlEncode(AbstractApi.HTTP_USER_AGENT);
var url = $"https://search.youku.com/api/search?keyword={keyword}&userAgent={ua}&site=1&categories=0&ftype=0&ob=0&pg=1";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = new List<YoukuVideo>();
@@ -170,7 +170,7 @@ public class YoukuApi : AbstractApi
// 获取影片信息https://openapi.youku.com/v2/shows/show.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&show_id=0b39c5b6569311e5b2ad
// 获取影片剧集信息https://openapi.youku.com/v2/shows/videos.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&ext=show&show_id=deea7e54c2594c489bfd
var url = $"https://openapi.youku.com/v2/shows/videos.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&ext=show&show_id={id}&page={page}&count={pageSize}";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<YoukuVideo>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -216,7 +216,7 @@ public class YoukuApi : AbstractApi
// 文档https://cloud.youku.com/docs?id=46
var url = $"https://openapi.youku.com/v2/videos/show_basic.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&video_id={vid}";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<YoukuEpisode>(this._jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -397,7 +397,7 @@ public class YoukuApi : AbstractApi
if (cookie == null)
{
var url = "https://log.mmstat.com/eg.js";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// 重新读取最新
@@ -416,7 +416,7 @@ public class YoukuApi : AbstractApi
if (tokenCookie == null || tokenEncCookie == null)
{
var url = "https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788";
var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// 重新读取最新

View File

@@ -1,11 +1,15 @@
using System;
using Jellyfin.Plugin.Danmu.Controllers.Entity;
using Jellyfin.Plugin.Danmu.Core;
using Jellyfin.Plugin.Danmu.Scrapers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Subtitles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Jellyfin.Plugin.Danmu.Scrapers;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.Persistence;
namespace Jellyfin.Plugin.Danmu
{
@@ -18,17 +22,19 @@ namespace Jellyfin.Plugin.Danmu
serviceCollection.AddHostedService<PluginStartup>();
serviceCollection.AddSingleton<ISubtitleProvider, DanmuSubtitleProvider>();
serviceCollection.AddSingleton<Jellyfin.Plugin.Danmu.Core.IFileSystem>((ctx) =>
{
return new Jellyfin.Plugin.Danmu.Core.FileSystem();
});
serviceCollection.AddSingleton<IFileSystem>(_ => new FileSystem());
serviceCollection.AddSingleton((ctx) =>
{
return new ScraperManager(ctx.GetRequiredService<ILoggerFactory>());
});
serviceCollection.AddSingleton((ctx) =>
{
return new LibraryManagerEventsHelper(ctx.GetRequiredService<IItemRepository>(), ctx.GetRequiredService<ILibraryManager>(), ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<Jellyfin.Plugin.Danmu.Core.IFileSystem>(), ctx.GetRequiredService<ScraperManager>());
return new LibraryManagerEventsHelper(ctx.GetRequiredService<IItemRepository>(), ctx.GetRequiredService<ILibraryManager>(), ctx.GetRequiredService<ILoggerFactory>(), ctx.GetRequiredService<IFileSystem>(), ctx.GetRequiredService<ScraperManager>());
});
serviceCollection.AddSingleton<FileCache<AnimeCacheItem>>((ctx) =>
{
var applicationPaths = ctx.GetRequiredService<IApplicationPaths>();
return new FileCache<AnimeCacheItem>(applicationPaths, ctx.GetRequiredService<ILoggerFactory>(),TimeSpan.FromDays(31), TimeSpan.FromSeconds(60));
});
}
}