feat: add douban rate limit #12
This commit is contained in:
parent
eede72d771
commit
c79bfc5bd1
|
@ -6,4 +6,5 @@ artifacts
|
|||
**/.DS_Store
|
||||
metashark/
|
||||
manifest_cn.json
|
||||
manifest.json
|
||||
manifest.json
|
||||
.vscode
|
||||
|
|
|
@ -59,6 +59,28 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestSearchBySuggest()
|
||||
{
|
||||
var keyword = "重返少年时";
|
||||
var api = new DoubanApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await api.SearchBySuggestAsync(keyword, CancellationToken.None);
|
||||
var str = result.ToJson();
|
||||
TestContext.WriteLine(result.ToJson());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TestContext.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetVideoBySidAsync()
|
||||
{
|
||||
|
|
|
@ -50,5 +50,29 @@ namespace Jellyfin.Plugin.MetaShark.Test
|
|||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetAnimeMetadata()
|
||||
{
|
||||
var info = new SeriesInfo() { Name = "命运-冠位嘉年华" };
|
||||
var doubanApi = new DoubanApi(loggerFactory);
|
||||
var tmdbApi = new TmdbApi(loggerFactory);
|
||||
var omdbApi = new OmdbApi(loggerFactory);
|
||||
var httpClientFactory = new DefaultHttpClientFactory();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var httpContextAccessorStub = new Mock<IHttpContextAccessor>();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var provider = new SeriesProvider(httpClientFactory, loggerFactory, libraryManagerStub.Object, httpContextAccessorStub.Object, doubanApi, tmdbApi, omdbApi);
|
||||
var result = await provider.GetMetadata(info, CancellationToken.None);
|
||||
Assert.AreEqual(result.Item.Name, "命运/冠位指定嘉年华 公元2020奥林匹亚英灵限界大祭");
|
||||
Assert.AreEqual(result.Item.OriginalTitle, "Fate/Grand Carnival");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var str = result.ToJson();
|
||||
Console.WriteLine(result.ToJson());
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ using Jellyfin.Plugin.MetaShark.Core;
|
|||
using System.Data;
|
||||
using TMDbLib.Objects.Movies;
|
||||
using System.Xml.Linq;
|
||||
using RateLimiter;
|
||||
using ComposableAsync;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Api
|
||||
{
|
||||
|
@ -50,6 +52,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
Regex regSid = new Regex(@"sid: (\d+?),", RegexOptions.Compiled);
|
||||
Regex regCat = new Regex(@"\[(.+?)\]", RegexOptions.Compiled);
|
||||
Regex regYear = new Regex(@"(\d{4})", RegexOptions.Compiled);
|
||||
Regex regTitle = new Regex(@"<title>([\w\W]+?)</title>", RegexOptions.Compiled);
|
||||
Regex regKeywordMeta = new Regex(@"<meta name=""keywords"" content=""(.+?)""", RegexOptions.Compiled);
|
||||
Regex regOriginalName = new Regex(@"原名[::](.+?)\s*?\/", RegexOptions.Compiled);
|
||||
Regex regDirector = new Regex(@"导演: (.+?)\n", RegexOptions.Compiled);
|
||||
Regex regWriter = new Regex(@"编剧: (.+?)\n", RegexOptions.Compiled);
|
||||
|
@ -75,6 +79,14 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
Regex regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
|
||||
Regex regCelebrityImdb = new Regex(@"imdb编号:\s+?(nm\d+)", RegexOptions.Compiled);
|
||||
|
||||
// 默认1秒请求1次
|
||||
private TimeLimiter _defaultTimeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000));
|
||||
// 未登录最多1分钟10次请求,不然5分钟后会被封ip
|
||||
private TimeLimiter _guestTimeConstraint = TimeLimiter.Compose(new CountByIntervalAwaitableConstraint(10, TimeSpan.FromMinutes(1)), new CountByIntervalAwaitableConstraint(1, TimeSpan.FromMilliseconds(5000)));
|
||||
// 登录后最多1分钟20次请求,不然会触发机器人检验
|
||||
private TimeLimiter _loginedTimeConstraint = TimeLimiter.Compose(new CountByIntervalAwaitableConstraint(20, TimeSpan.FromMinutes(1)), new CountByIntervalAwaitableConstraint(1, TimeSpan.FromMilliseconds(3000)));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DoubanApi"/> class.
|
||||
/// </summary>
|
||||
|
@ -86,9 +98,9 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
|
||||
var handler = new HttpClientHandlerEx();
|
||||
this._cookieContainer = handler.CookieContainer;
|
||||
httpClient = new HttpClient(handler, true);
|
||||
httpClient = new HttpClient(handler);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", HTTP_USER_AGENT);
|
||||
httpClient.DefaultRequestHeaders.Add("Origin", "https://movie.douban.com");
|
||||
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
|
||||
}
|
||||
|
@ -162,7 +174,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
|
||||
|
||||
EnsureLoadDoubanCookie();
|
||||
// LimitRequestFrequently(2000);
|
||||
await LimitRequestFrequently();
|
||||
|
||||
var encodedKeyword = HttpUtility.UrlEncode(keyword);
|
||||
var url = $"https://www.douban.com/search?cat=1002&q={encodedKeyword}";
|
||||
|
@ -220,6 +232,56 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
return list;
|
||||
}
|
||||
|
||||
public async Task<List<DoubanSubject>> SearchBySuggestAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<DoubanSubject>();
|
||||
if (string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
EnsureLoadDoubanCookie();
|
||||
await LimitRequestFrequently();
|
||||
|
||||
var encodedKeyword = HttpUtility.UrlEncode(keyword);
|
||||
var url = $"https://www.douban.com/j/search_suggest?q={encodedKeyword}";
|
||||
|
||||
using (var requestMessage = new HttpRequestMessage(HttpMethod.Get, url))
|
||||
{
|
||||
requestMessage.Headers.Add("Origin", "https://www.douban.com");
|
||||
requestMessage.Headers.Add("Referer", "https://www.douban.com/");
|
||||
|
||||
var response = await httpClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this._logger.LogWarning("douban suggest请求失败. keyword: {0} statusCode: {1}", keyword, response.StatusCode);
|
||||
return list;
|
||||
}
|
||||
|
||||
JsonSerializerOptions? serializeOptions = null;
|
||||
var result = await response.Content.ReadFromJsonAsync<DoubanSuggestResult>(serializeOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result != null && result.Cards != null)
|
||||
{
|
||||
foreach (var suggest in result.Cards)
|
||||
{
|
||||
if (suggest.Type != "movie")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var movie = new DoubanSubject();
|
||||
movie.Sid = suggest.Sid;
|
||||
movie.Name = suggest.Title;
|
||||
movie.Year = suggest.Year.ToInt();
|
||||
list.Add(movie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<DoubanSubject?> GetMovieAsync(string sid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sid))
|
||||
|
@ -236,7 +298,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
}
|
||||
|
||||
EnsureLoadDoubanCookie();
|
||||
// LimitRequestFrequently();
|
||||
await LimitRequestFrequently();
|
||||
|
||||
var url = $"https://movie.douban.com/subject/{sid}/";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -253,14 +315,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
if (contentNode != null)
|
||||
{
|
||||
var nameStr = contentNode.GetText("h1>span:first-child") ?? string.Empty;
|
||||
var match = this.regNameMath.Match(nameStr);
|
||||
var name = string.Empty;
|
||||
var orginalName = string.Empty;
|
||||
if (match.Success && match.Groups.Count == 3)
|
||||
{
|
||||
name = match.Groups[1].Value;
|
||||
orginalName = match.Groups[2].Value;
|
||||
}
|
||||
var name = GetTitle(body);
|
||||
var orginalName = nameStr.Replace(name, "").Trim();
|
||||
var yearStr = contentNode.GetText("h1>span.year") ?? string.Empty;
|
||||
var year = yearStr.GetMatchGroup(this.regYear);
|
||||
var rating = contentNode.GetText("div.rating_self strong.rating_num") ?? "0";
|
||||
|
@ -347,7 +403,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
}
|
||||
|
||||
EnsureLoadDoubanCookie();
|
||||
// LimitRequestFrequently();
|
||||
await LimitRequestFrequently();
|
||||
|
||||
var list = new List<DoubanCelebrity>();
|
||||
var url = $"https://movie.douban.com/subject/{sid}/celebrities";
|
||||
|
@ -493,7 +549,6 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
|
||||
|
||||
EnsureLoadDoubanCookie();
|
||||
// LimitRequestFrequently();
|
||||
|
||||
|
||||
keyword = HttpUtility.UrlEncode(keyword);
|
||||
|
@ -547,7 +602,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
}
|
||||
|
||||
EnsureLoadDoubanCookie();
|
||||
// LimitRequestFrequently();
|
||||
await LimitRequestFrequently();
|
||||
|
||||
var list = new List<DoubanPhoto>();
|
||||
var url = $"https://movie.douban.com/subject/{sid}/photos?type=W&start=0&sortby=size&size=a&subtype=a";
|
||||
|
@ -598,24 +653,105 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
return list;
|
||||
}
|
||||
|
||||
|
||||
protected void LimitRequestFrequently(int interval = 1000)
|
||||
public async Task<bool> CheckLoginAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var diff = 0;
|
||||
lock (_lock)
|
||||
{
|
||||
var ts = DateTime.Now - lastRequestTime;
|
||||
diff = (int)(interval - ts.TotalMilliseconds);
|
||||
if (diff > 0)
|
||||
{
|
||||
this._logger.LogInformation("请求太频繁,等待{0}毫秒后继续执行...", diff);
|
||||
Thread.Sleep(diff);
|
||||
}
|
||||
EnsureLoadDoubanCookie();
|
||||
|
||||
lastRequestTime = DateTime.Now;
|
||||
var url = "https://www.douban.com/mine/";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
var requestUrl = response.RequestMessage?.RequestUri?.ToString();
|
||||
if (requestUrl == null || requestUrl.Contains("login") || requestUrl.Contains("sec.douban.com"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async Task LimitRequestFrequently()
|
||||
{
|
||||
if (IsEnableAvoidRiskControl())
|
||||
{
|
||||
var configCookie = Plugin.Instance?.Configuration.DoubanCookies.Trim() ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(configCookie))
|
||||
{
|
||||
await this._loginedTimeConstraint;
|
||||
}
|
||||
else
|
||||
{
|
||||
await this._guestTimeConstraint;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await this._defaultTimeConstraint;
|
||||
}
|
||||
|
||||
// var diff = 0;
|
||||
// Double interval = 0.0;
|
||||
// if (IsEnableAvoidRiskControl())
|
||||
// {
|
||||
// var configCookie = Plugin.Instance?.Configuration.DoubanCookies.Trim() ?? string.Empty;
|
||||
// if (string.IsNullOrEmpty(configCookie))
|
||||
// {
|
||||
// interval = 3000;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// interval = 6000;
|
||||
// }
|
||||
// // // 启用防止封禁
|
||||
// // this._logger.LogWarning("thread开始等待." + Thread.CurrentThread.ManagedThreadId);
|
||||
// // await this._limitTimeConstraint;
|
||||
// // this._logger.LogWarning("thread等待结束." + Thread.CurrentThread.ManagedThreadId);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// interval = 1000;
|
||||
// // 默认限制
|
||||
// // await this._defaultTimeConstraint;
|
||||
// }
|
||||
|
||||
// this._logger.LogWarning("thread进入." + Thread.CurrentThread.ManagedThreadId);
|
||||
// lock (_lock)
|
||||
// {
|
||||
// this._logger.LogWarning("thread开始等待." + Thread.CurrentThread.ManagedThreadId);
|
||||
|
||||
// lastRequestTime = lastRequestTime.AddMilliseconds(interval);
|
||||
// diff = (int)(lastRequestTime - DateTime.Now).TotalMilliseconds;
|
||||
// if (diff <= 0)
|
||||
// {
|
||||
// lastRequestTime = DateTime.Now;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (diff > 0)
|
||||
// {
|
||||
// this._logger.LogInformation("请求太频繁,等待{0}毫秒后继续执行..." + Thread.CurrentThread.ManagedThreadId, diff);
|
||||
// // Thread.Sleep(diff);
|
||||
// await Task.Delay(diff);
|
||||
// }
|
||||
|
||||
// this._logger.LogWarning("thread等待结束." + Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
|
||||
private string GetTitle(string body)
|
||||
{
|
||||
var title = string.Empty;
|
||||
|
||||
var keyword = Match(body, regKeywordMeta);
|
||||
if (!string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
title = keyword.Split(",").FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
return title.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
title = Match(body, regTitle);
|
||||
return title.Replace("(豆瓣)", "").Trim();
|
||||
}
|
||||
|
||||
private string? GetText(IElement el, string css)
|
||||
{
|
||||
|
@ -650,6 +786,11 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
private bool IsEnableAvoidRiskControl()
|
||||
{
|
||||
return Plugin.Instance?.Configuration.EnableDoubanAvoidRiskControl ?? false;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
@ -661,6 +802,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
|
|||
{
|
||||
if (disposing)
|
||||
{
|
||||
httpClient.Dispose();
|
||||
_memoryCache.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Api.Http
|
||||
{
|
||||
public class LoggingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ILogger<LoggingHandler> _logger;
|
||||
public LoggingHandler(HttpMessageHandler innerHandler, ILoggerFactory loggerFactory)
|
||||
: base(innerHandler)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<LoggingHandler>();
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation((request.RequestUri?.ToString() ?? string.Empty));
|
||||
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -25,7 +25,12 @@ public enum SomeOptions
|
|||
/// </summary>
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
||||
public string Version { get; } = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
|
||||
|
||||
public string DoubanCookies { get; set; } = string.Empty;
|
||||
|
||||
public bool EnableDoubanAvoidRiskControl { get; set; } = false;
|
||||
|
||||
public bool EnableTmdb { get; set; } = true;
|
||||
|
||||
public bool EnableTmdbSearch { get; set; } = false;
|
||||
|
@ -34,7 +39,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||
|
||||
public string TmdbHost { get; set; } = string.Empty;
|
||||
|
||||
public string DoubanCookies { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public int MaxCastMembers { get; set; } = 15;
|
||||
|
||||
|
|
|
@ -22,12 +22,33 @@
|
|||
</div>
|
||||
|
||||
<form id="TemplateConfigForm">
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie</label>
|
||||
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
|
||||
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
|
||||
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用分号“;”分隔格式cookie.</div>
|
||||
</div>
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend>
|
||||
<h3>豆瓣</h3>
|
||||
</legend>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie<span
|
||||
id="login_invalid"
|
||||
style="color: red; margin-left: 8px; display: none;">(已失效)</span></label>
|
||||
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies"
|
||||
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea>
|
||||
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用(www.douban.com)分号“;”分隔格式cookie.</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label class="emby-checkbox-label" for="EnableDoubanAvoidRiskControl">
|
||||
<input id="EnableDoubanAvoidRiskControl" name="EnableDoubanAvoidRiskControl"
|
||||
type="checkbox" is="emby-checkbox" />
|
||||
<span class="checkboxLabel" style="position:relative">启用防封禁
|
||||
<!-- <span
|
||||
style="width:24px;color:red;font-size:12px;position: absolute;top:-3px;margin-left:3px;">实险</span> -->
|
||||
<img style="position: absolute; top:-12px; width: 32px; height:32px"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABS9JREFUeF7tmWtoHFUUx/9ndne2NmLph5q0yc4m1SSzMS2opRIpGEGLtWotqBS0NIIgCCotVD+aItSi+EB80S8V0YgKWouPNhUMqbWKsWqpya5pQjITRb8IKsruzO4cuXcf7m42yTRkgzs799Puzp17z/md/zn3sYQ6b1Tn/sMH4Cugzgn4KVDnAvCLoJ8CfgrUOQE/BepcAP4q4KeAnwJ1TsBPgToXwMWtAudbG5oCGfsVIlwHYF2NwPsFwIhuWDsq2es6BUbWYWVDUP2CgKtrxPFZZuqGNctf1wDGNLWfgCdq1fmc3e/phnVPsQ+uASQ09TwDV9UyAAb/FjPspkUBiGsq17LzedvL08C1AnwAFRTAwIGYYfULuvFI6HMQ9f7fVbKkCqg2ADF+gHlaVULHk449sBSAaw7AUiuspgAQ0N9pWAeWMsWWA8AZBgaJ0Q4F3WBsrFAXvgVwAcA0QJcBvBbM14NoTaEv8xARDZUAyD8srTXfAJgUYxFoNYObAdw6Vy2qKgCGcihmpk7kJ59sa2i0HPs1MO4s2M60r9NMPV9u4MR6rLLt0EEQPSSeVdq1id+LNmSmw3iky7SOlo81poWuJZDYtN1e/qxqAEbbVkSDdqYpHVA2Bck53TFlf581WBozIg1hDOimda/4KM4VQcfeS8A/9qXWU92jsKTUNXVM+M+Oc6P4HptJD8lxWoLZFUZReuWOlHCfPm29JX4ajai7CLgDhHOZoDLQPZk0pqIr1yY5LYKxoRhC1QAkoupOZryfn0xh54YOMz2cc+qYiIZCdFvHdOrj8Sasyajq1wDasv3ptG6ktkhnWtSdipIdZ55V5g8AwyA6pTg84xAGCk4SvapPp6SKEpp6hIG+ZQEQ19Q3AcjoSuOZX4qZ9sMSQCT0spB2QLVWtV/An/GI+iQRtGLDHMbxmGm9PdESbrcV/mkBAFlsQJIJBjv4kcCD6YB6tHvq71/z41Y6v1RNAXFN/RBChrlGwDudhrVLyldTDxKwVTesTcVOV/o8fiXCGUtNLgSAGR+kQfs3mKmJgsMtwV5SaIdu2Htz8846wFUNwFg09BwxyYlzGnhRN+xHC1Jk3q6b9uXyezT0ADv/qWUWiFyVnycFTuiGdYt0snVFKxynTwG2MbAZzEO6acv6sawKSGjh9YDzJYMaxeQOaGuXkTqZBRA6yaCbHIV7uqbsr2RalG6t/2Kmu4hY3DVcg+ydQ3sJgNIUe0Y3rMcKcIvyXBTPQuGscIRflAISWmg3g94oj1SxgeLZePMlLelAZjeYzuaXw4QW3MJQTuXe/UQ3rO35vplApofBqxUok51G6rMKUSs5v4uIAughoEM3LFlAZZQZm5kwzMSDRIHfY1PJKQk5Gt4P5qeL7b5oAHFNfRzAoUr5Wi5RdpQHYz+nZAETreI+AHTYgXM4ZthiM1RoQsqK4+xhQB6u8q3c4EQkvI+Jn2Xm+2Om/XrJGJFQHxEdyb8zqoVvVsCDiwYQ10IvACTzuGJjlmt0oREJ+Z4B8AOAKAixOXaCooSfI8ZZ8S5D3jHG5pwHOMbAdyLSIGzL9yNghkEfMXOjQtjIwBXyWZFdTNRMQPtcQOe8D4hr6rsA7p7HqJp85CoF4lH1UzBklfVSc3UlVr6h8RIAAAtfinrl6qtS4Fxdi+c2D3sAtHok+kvzx4hHYMxyw/WtsA/AowR8BXg0sK7d8hXgGpVHO/oK8GhgXbvlK8A1Ko929BXg0cC6dstXgGtUHu3oK8CjgXXtlq8A16g82vFfwx5yXxq/mYIAAAAASUVORK5CYII="
|
||||
alt="beta" />
|
||||
</span>
|
||||
</label>
|
||||
<div class="fieldDescription">勾选后,刮削会变慢,适合刮削大量影片时使用,建议搭配网站cookie一起使用</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="verticalSection verticalSection-extrabottompadding">
|
||||
<legend>
|
||||
|
@ -80,10 +101,14 @@
|
|||
$('#current_version').text("v" + config.Version);
|
||||
|
||||
document.querySelector('#DoubanCookies').value = config.DoubanCookies;
|
||||
document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl;
|
||||
document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
|
||||
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
|
||||
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
|
||||
document.querySelector('#TmdbHost').value = config.TmdbHost;
|
||||
|
||||
checkDoubanLogin();
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
});
|
||||
|
@ -93,18 +118,38 @@
|
|||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
|
||||
config.DoubanCookies = document.querySelector('#DoubanCookies').value;
|
||||
config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked;
|
||||
config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
|
||||
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
|
||||
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
|
||||
config.TmdbHost = document.querySelector('#TmdbHost').value;
|
||||
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
|
||||
checkDoubanLogin();
|
||||
});
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
function checkDoubanLogin() {
|
||||
let cookie = document.querySelector('#DoubanCookies').value
|
||||
if (!cookie || !$.trim(cookie)) {
|
||||
$('#login_invalid').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$.getJSON("/plugin/metashark/douban/checklogin", function (resp) {
|
||||
if (resp && resp.code != 1) {
|
||||
$('#login_invalid').show();
|
||||
} else {
|
||||
$('#login_invalid').hide();
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Threading;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -19,6 +20,8 @@ using System.Runtime.InteropServices;
|
|||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Jellyfin.Plugin.MetaShark.Api;
|
||||
using Jellyfin.Plugin.MetaShark.Model;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Controllers
|
||||
{
|
||||
|
@ -27,15 +30,17 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
|||
[Route("/plugin/metashark")]
|
||||
public class MetaSharkController : ControllerBase
|
||||
{
|
||||
private readonly DoubanApi _doubanApi;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MetaSharkController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
|
||||
public MetaSharkController(IHttpClientFactory httpClientFactory)
|
||||
public MetaSharkController(IHttpClientFactory httpClientFactory, DoubanApi doubanApi)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
this._httpClientFactory = httpClientFactory;
|
||||
this._doubanApi = doubanApi;
|
||||
}
|
||||
|
||||
|
||||
|
@ -71,6 +76,17 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
|
|||
return stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 代理访问图片.
|
||||
/// </summary>
|
||||
[Route("douban/checklogin")]
|
||||
[HttpGet]
|
||||
public async Task<ApiResult> CheckDoubanLogin()
|
||||
{
|
||||
var isLogin = await _doubanApi.CheckLoginAsync(CancellationToken.None);
|
||||
return new ApiResult(isLogin ? 1 : 0, isLogin ? "logined" : "not login");
|
||||
}
|
||||
|
||||
private HttpClient GetHttpClient()
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
|
|
|
@ -75,7 +75,7 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
}
|
||||
|
||||
// 假如Anitomy解析不到year,尝试使用jellyfin默认parser,看能不能解析成功
|
||||
if (parseResult.Year == null)
|
||||
if (parseResult.Year == null && !IsAnime(fileName))
|
||||
{
|
||||
var nativeParseResult = ParseMovie(fileName);
|
||||
if (nativeParseResult.Year != null)
|
||||
|
@ -165,6 +165,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
|
|||
return true;
|
||||
}
|
||||
|
||||
if (Regex.Match(name, @"\[.+\].*?\[.+?\]", RegexOptions.IgnoreCase).Success)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
|
||||
|
@ -8,35 +7,28 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
||||
<PackageReference Include="AnitomySharp" Version="0.2.0" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
|
||||
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Configuration\configPage.html" />
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Vendor\TMDbLib\TMDbLib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
|
@ -0,0 +1,17 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Model;
|
||||
|
||||
public class ApiResult
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
[JsonPropertyName("msg")]
|
||||
public string Msg { get; set; } = string.Empty;
|
||||
|
||||
public ApiResult(int code, string msg = "")
|
||||
{
|
||||
this.Code = code;
|
||||
this.Msg = msg;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Jellyfin.Plugin.MetaShark.Core;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Model;
|
||||
|
||||
public class DoubanSuggest
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
[JsonPropertyName("year")]
|
||||
public string Year { get; set; } = string.Empty;
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public string Sid
|
||||
{
|
||||
get
|
||||
{
|
||||
var regSid = new Regex(@"subject\/(\d+?)\/", RegexOptions.Compiled);
|
||||
return this.Url.GetMatchGroup(regSid);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin.Plugin.MetaShark.Model;
|
||||
|
||||
public class DoubanSuggestResult
|
||||
{
|
||||
[JsonPropertyName("cards")]
|
||||
public List<DoubanSuggest>? Cards { get; set; }
|
||||
}
|
|
@ -82,36 +82,51 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
|
||||
this.Log($"GuessByDouban of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
|
||||
var result = await this._doubanApi.SearchAsync(searchName, cancellationToken).ConfigureAwait(false);
|
||||
var jw = new JaroWinkler();
|
||||
foreach (var item in result)
|
||||
List<DoubanSubject> result;
|
||||
DoubanSubject? item;
|
||||
|
||||
// 假如存在年份,先通过suggest接口查找,减少搜索页访问次数,避免封禁(suggest没法区分电影或电视剧,排序也比搜索页差些)
|
||||
if (config.EnableDoubanAvoidRiskControl)
|
||||
{
|
||||
if (info is MovieInfo && item.Category != "电影")
|
||||
if (info.Year != null && info.Year > 0)
|
||||
{
|
||||
continue;
|
||||
result = await this._doubanApi.SearchBySuggestAsync(searchName, cancellationToken).ConfigureAwait(false);
|
||||
item = result.Where(x => x.Year == info.Year && x.Name == searchName).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
this.Log($"GuessByDouban of [name]: {searchName} found Sid: {item.Sid} (suggest)");
|
||||
return item.Sid;
|
||||
}
|
||||
item = result.Where(x => x.Year == info.Year).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
this.Log($"GuessByDouban of [name]: {searchName} found Sid: {item.Sid} (suggest)");
|
||||
return item.Sid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info is SeriesInfo && item.Category != "电视剧")
|
||||
// 通过搜索页面查找
|
||||
result = await this._doubanApi.SearchAsync(searchName, cancellationToken).ConfigureAwait(false);
|
||||
var cat = info is MovieInfo ? "电影" : "电视剧";
|
||||
|
||||
// 优先返回对应年份的电影
|
||||
if (info.Year != null && info.Year > 0)
|
||||
{
|
||||
item = result.Where(x => x.Category == cat && x.Year == info.Year).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
||||
|
||||
// 不存在年份需要比较时,直接返回
|
||||
if (info.Year == null || info.Year == 0)
|
||||
{
|
||||
this.Log($"GuessByDouban of [name] found Sid: {item.Sid}");
|
||||
return item.Sid;
|
||||
}
|
||||
|
||||
if (info.Year == item.Year)
|
||||
{
|
||||
this.Log($"GuessByDouban of [name] found Sid: {item.Sid}");
|
||||
this.Log($"GuessByDouban of [name]: {searchName} found Sid: {item.Sid}");
|
||||
return item.Sid;
|
||||
}
|
||||
}
|
||||
|
||||
// 不存在年份时,返回第一个
|
||||
item = result.Where(x => x.Category == cat).FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
this.Log($"GuessByDouban of [name]: {searchName} found Sid: {item.Sid}");
|
||||
return item.Sid;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -125,24 +140,35 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
}
|
||||
|
||||
this.Log($"GuestDoubanSeasonByYear of [name]: {name} [year]: {year}");
|
||||
var result = await this._doubanApi.SearchAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
var jw = new JaroWinkler();
|
||||
foreach (var item in result)
|
||||
|
||||
// 先通过suggest接口查找,减少搜索页访问次数,避免封禁(suggest没法区分电影或电视剧,排序也比搜索页差些)
|
||||
if (config.EnableDoubanAvoidRiskControl)
|
||||
{
|
||||
if (item.Category != "电视剧")
|
||||
var suggestResult = await this._doubanApi.SearchBySuggestAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
var suggestItem = suggestResult.Where(x => x.Year == year && x.Name == name).FirstOrDefault();
|
||||
if (suggestItem != null)
|
||||
{
|
||||
continue;
|
||||
this.Log($"GuestDoubanSeasonByYear of [name] found Sid: \"{suggestItem.Sid}\" (suggest)");
|
||||
return suggestItem.Sid;
|
||||
}
|
||||
|
||||
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
||||
|
||||
if (year == item.Year)
|
||||
suggestItem = suggestResult.Where(x => x.Year == year).FirstOrDefault();
|
||||
if (suggestItem != null)
|
||||
{
|
||||
this.Log($"GuestDoubanSeasonByYear of [name] found Sid: \"{item.Sid}\"");
|
||||
return item.Sid;
|
||||
this.Log($"GuestDoubanSeasonByYear of [name] found Sid: \"{suggestItem.Sid}\" (suggest)");
|
||||
return suggestItem.Sid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 通过搜索页面查找
|
||||
var result = await this._doubanApi.SearchAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
var item = result.Where(x => x.Category == "电视剧" && x.Year == year).FirstOrDefault();
|
||||
if (item != null && !string.IsNullOrEmpty(item.Sid))
|
||||
{
|
||||
this.Log($"GuestDoubanSeasonByYear of [name] found Sid: \"{item.Sid}\"");
|
||||
return item.Sid;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -158,24 +184,24 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
|
||||
|
||||
this.Log($"GuestByTmdb of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
|
||||
var jw = new JaroWinkler();
|
||||
|
||||
switch (info)
|
||||
{
|
||||
case MovieInfo:
|
||||
var movieResults = await this._tmdbApi.SearchMovieAsync(searchName, info.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var item in movieResults)
|
||||
var movieItem = movieResults.FirstOrDefault();
|
||||
if (movieItem != null)
|
||||
{
|
||||
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
||||
return item.Id.ToString(CultureInfo.InvariantCulture);
|
||||
return movieItem.Id.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
break;
|
||||
case SeriesInfo:
|
||||
var seriesResults = await this._tmdbApi.SearchSeriesAsync(searchName, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var item in seriesResults)
|
||||
var seriesItem = seriesResults.FirstOrDefault();
|
||||
if (seriesItem != null)
|
||||
{
|
||||
// bt种子都是英文名,但电影是中日韩泰印法地区时,都不适用相似匹配,去掉限制
|
||||
return item.Id.ToString(CultureInfo.InvariantCulture);
|
||||
return seriesItem.Id.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||
{
|
||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
|
||||
if (primary == null)
|
||||
if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
|
||||
{
|
||||
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
|
||||
if (primary == null)
|
||||
if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
|
||||
{
|
||||
return Enumerable.Empty<RemoteImageInfo>();
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
|
|||
return list;
|
||||
}
|
||||
|
||||
return photo.Where(x => x.Width > x.Height * 1.3).Select(x =>
|
||||
return photo.Where(x => x.Width > x.Height * 1.3 && !string.IsNullOrEmpty(x.Large)).Select(x =>
|
||||
{
|
||||
return new RemoteImageInfo
|
||||
{
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ComposableAsync.Core.Test")]
|
||||
|
43
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Awaitable/DispatcherAwaiter.cs
vendored
Normal file
43
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Awaitable/DispatcherAwaiter.cs
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher awaiter, making a dispatcher awaitable
|
||||
/// </summary>
|
||||
public struct DispatcherAwaiter : INotifyCompletion
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher never is synchronous
|
||||
/// </summary>
|
||||
public bool IsCompleted => false;
|
||||
|
||||
private readonly IDispatcher _Dispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a NotifyCompletion fom a dispatcher
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
public DispatcherAwaiter(IDispatcher dispatcher)
|
||||
{
|
||||
_Dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch on complete
|
||||
/// </summary>
|
||||
/// <param name="continuation"></param>
|
||||
[SecuritySafeCritical]
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
_Dispatcher.Dispatch(continuation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No Result
|
||||
/// </summary>
|
||||
public void GetResult() { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="DelegatingHandler"/> implementation based on <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
internal class DispatcherDelegatingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IDispatcher _Dispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Build an <see cref="DelegatingHandler"/> from a <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
public DispatcherDelegatingHandler(IDispatcher dispatcher)
|
||||
{
|
||||
_Dispatcher = dispatcher;
|
||||
InnerHandler = new HttpClientHandler();
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return _Dispatcher.Enqueue(() => base.SendAsync(request, cancellationToken), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
73
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Dispatcher/ComposedDispatcher.cs
vendored
Normal file
73
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Dispatcher/ComposedDispatcher.cs
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
internal class ComposedDispatcher : IDispatcher, IAsyncDisposable
|
||||
{
|
||||
private readonly IDispatcher _First;
|
||||
private readonly IDispatcher _Second;
|
||||
|
||||
public ComposedDispatcher(IDispatcher first, IDispatcher second)
|
||||
{
|
||||
_First = first;
|
||||
_Second = second;
|
||||
}
|
||||
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
_First.Dispatch(() => _Second.Dispatch(action));
|
||||
}
|
||||
|
||||
public async Task Enqueue(Action action)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<T> action)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task Enqueue(Func<Task> action)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action));
|
||||
}
|
||||
|
||||
public async Task Enqueue(Func<Task> action, CancellationToken cancellationToken)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task Enqueue(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
await _First.Enqueue(() => _Second.Enqueue(action, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public IDispatcher Clone() => new ComposedDispatcher(_First, _Second);
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
return Task.WhenAll(DisposeAsync(_First), DisposeAsync(_Second));
|
||||
}
|
||||
|
||||
private static Task DisposeAsync(IDispatcher disposable) => (disposable as IAsyncDisposable)?.DisposeAsync() ?? Task.CompletedTask;
|
||||
|
||||
}
|
||||
}
|
63
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Dispatcher/DispatcherAdapter.cs
vendored
Normal file
63
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Dispatcher/DispatcherAdapter.cs
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
internal class DispatcherAdapter : IDispatcher
|
||||
{
|
||||
private readonly IBasicDispatcher _BasicDispatcher;
|
||||
|
||||
public DispatcherAdapter(IBasicDispatcher basicDispatcher)
|
||||
{
|
||||
_BasicDispatcher = basicDispatcher;
|
||||
}
|
||||
|
||||
public IDispatcher Clone() => new DispatcherAdapter(_BasicDispatcher.Clone());
|
||||
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
_BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task Enqueue(Action action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<T> action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<Task> action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, CancellationToken.None);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
|
||||
public Task Enqueue(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<Task> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return _BasicDispatcher.Enqueue(action, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
74
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Dispatcher/NullDispatcher.cs
vendored
Normal file
74
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Dispatcher/NullDispatcher.cs
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcher"/> that run actions synchronously
|
||||
/// </summary>
|
||||
public sealed class NullDispatcher: IDispatcher
|
||||
{
|
||||
private NullDispatcher() { }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a static null dispatcher
|
||||
/// </summary>
|
||||
public static IDispatcher Instance { get; } = new NullDispatcher();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
action();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Enqueue(Action action)
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<T> Enqueue<T>(Func<T> action)
|
||||
{
|
||||
return Task.FromResult(action());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Enqueue(Func<Task> action)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
|
||||
public Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(action());
|
||||
}
|
||||
|
||||
public Task Enqueue(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Enqueue(Func<Task> action, CancellationToken cancellationToken)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken)
|
||||
{
|
||||
return await action();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDispatcher Clone() => Instance;
|
||||
}
|
||||
}
|
90
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherExtension.cs
vendored
Normal file
90
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherExtension.cs
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcher"/> extension methods provider
|
||||
/// </summary>
|
||||
public static class DispatcherExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns awaitable to enter in the dispatcher context
|
||||
/// This extension method make a dispatcher awaitable
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <returns></returns>
|
||||
public static DispatcherAwaiter GetAwaiter(this IDispatcher dispatcher)
|
||||
{
|
||||
return new DispatcherAwaiter(dispatcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a composed dispatcher applying the given dispatcher
|
||||
/// after the first one
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="other"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher Then(this IDispatcher dispatcher, IDispatcher other)
|
||||
{
|
||||
if (dispatcher == null)
|
||||
throw new ArgumentNullException(nameof(dispatcher));
|
||||
|
||||
if (other == null)
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
|
||||
return new ComposedDispatcher(dispatcher, other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a composed dispatcher applying the given dispatchers sequentially
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="others"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher Then(this IDispatcher dispatcher, params IDispatcher[] others)
|
||||
{
|
||||
return dispatcher.Then((IEnumerable<IDispatcher>)others);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a composed dispatcher applying the given dispatchers sequentially
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="others"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher Then(this IDispatcher dispatcher, IEnumerable<IDispatcher> others)
|
||||
{
|
||||
if (dispatcher == null)
|
||||
throw new ArgumentNullException(nameof(dispatcher));
|
||||
|
||||
if (others == null)
|
||||
throw new ArgumentNullException(nameof(others));
|
||||
|
||||
return others.Aggregate(dispatcher, (cum, val) => cum.Then(val));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="DelegatingHandler"/> from an <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <returns></returns>
|
||||
public static DelegatingHandler AsDelegatingHandler(this IDispatcher dispatcher)
|
||||
{
|
||||
return new DispatcherDelegatingHandler(dispatcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="IDispatcher"/> from an <see cref="IBasicDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="basicDispatcher"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher ToFullDispatcher(this IBasicDispatcher @basicDispatcher)
|
||||
{
|
||||
return new DispatcherAdapter(@basicDispatcher);
|
||||
}
|
||||
}
|
||||
}
|
19
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherManager/IDispatcherManager.cs
vendored
Normal file
19
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherManager/IDispatcherManager.cs
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher manager
|
||||
/// </summary>
|
||||
public interface IDispatcherManager : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// true if the Dispatcher should be released
|
||||
/// </summary>
|
||||
bool DisposeDispatcher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a consumable Dispatcher
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDispatcher GetDispatcher();
|
||||
}
|
||||
}
|
36
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherManager/MonoDispatcherManager.cs
vendored
Normal file
36
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherManager/MonoDispatcherManager.cs
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcherManager"/> implementation based on single <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
public sealed class MonoDispatcherManager : IDispatcherManager
|
||||
{
|
||||
/// <inheritdoc cref="IDispatcherManager"/>
|
||||
public bool DisposeDispatcher { get; }
|
||||
|
||||
/// <inheritdoc cref="IDispatcherManager"/>
|
||||
public IDispatcher GetDispatcher() => _Dispatcher;
|
||||
|
||||
private readonly IDispatcher _Dispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Create
|
||||
/// </summary>
|
||||
/// <param name="dispatcher"></param>
|
||||
/// <param name="shouldDispose"></param>
|
||||
public MonoDispatcherManager(IDispatcher dispatcher, bool shouldDispose = false)
|
||||
{
|
||||
_Dispatcher = dispatcher;
|
||||
DisposeDispatcher = shouldDispose;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDispatcherManager"/>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
return DisposeDispatcher && (_Dispatcher is IAsyncDisposable disposable) ?
|
||||
disposable.DisposeAsync() : Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
18
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherProviderExtension.cs
vendored
Normal file
18
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/DispatcherProviderExtension.cs
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IDispatcherProvider"/> extension
|
||||
/// </summary>
|
||||
public static class DispatcherProviderExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the underlying <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
/// <param name="dispatcherProvider"></param>
|
||||
/// <returns></returns>
|
||||
public static IDispatcher GetAssociatedDispatcher(this IDispatcherProvider dispatcherProvider)
|
||||
{
|
||||
return dispatcherProvider?.Dispatcher ?? NullDispatcher.Instance;
|
||||
}
|
||||
}
|
||||
}
|
48
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Disposable/ComposableAsyncDisposable.cs
vendored
Normal file
48
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Disposable/ComposableAsyncDisposable.cs
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IAsyncDisposable"/> implementation aggregating other <see cref="IAsyncDisposable"/>
|
||||
/// </summary>
|
||||
public sealed class ComposableAsyncDisposable : IAsyncDisposable
|
||||
{
|
||||
private readonly ConcurrentQueue<IAsyncDisposable> _Disposables;
|
||||
|
||||
/// <summary>
|
||||
/// Build an empty ComposableAsyncDisposable
|
||||
/// </summary>
|
||||
public ComposableAsyncDisposable()
|
||||
{
|
||||
_Disposables = new ConcurrentQueue<IAsyncDisposable>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an <see cref="IAsyncDisposable"/> to the ComposableAsyncDisposable
|
||||
/// and returns it
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="disposable"></param>
|
||||
/// <returns></returns>
|
||||
public T Add<T>(T disposable) where T: IAsyncDisposable
|
||||
{
|
||||
if (disposable == null)
|
||||
return default(T);
|
||||
|
||||
_Disposables.Enqueue(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose all the resources asynchronously
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
var tasks = _Disposables.ToArray().Select(disposable => disposable.DisposeAsync()).ToArray();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
}
|
17
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Disposable/IAsyncDisposable.cs
vendored
Normal file
17
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/Disposable/IAsyncDisposable.cs
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronous version of IDisposable
|
||||
/// For reference see discussion: https://github.com/dotnet/roslyn/issues/114
|
||||
/// </summary>
|
||||
public interface IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs asynchronously application-defined tasks associated with freeing,
|
||||
/// releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
Task DisposeAsync();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Simplified version of <see cref="IDispatcher"/> that can be converted
|
||||
/// to a <see cref="IDispatcher"/> using the ToFullDispatcher extension method
|
||||
/// </summary>
|
||||
public interface IBasicDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Clone dispatcher
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IBasicDispatcher Clone();
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the function and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// /// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the action and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Action action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Func<Task> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the original task
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatcher executes an action or a function
|
||||
/// on its own context
|
||||
/// </summary>
|
||||
public interface IDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute action on dispatcher context in a
|
||||
/// none-blocking way
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
void Dispatch(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the action and return a task corresponding to
|
||||
/// the completion of the action
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the function and return a task corresponding to
|
||||
/// the result of the function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<T> action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding to
|
||||
/// the completion of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Func<Task> action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the original task
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<Task<T>> action);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the function and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// /// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<T> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the action and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Action action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the task
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task Enqueue(Func<Task> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue the task and return a task corresponding
|
||||
/// to the execution of the original task
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<T> Enqueue<T>(Func<Task<T>> action, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Clone dispatcher
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDispatcher Clone();
|
||||
}
|
||||
}
|
13
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/IDispatcherProvider.cs
vendored
Normal file
13
Jellyfin.Plugin.MetaShark/Vendor/ComposableAsync.Core/IDispatcherProvider.cs
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace ComposableAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the fiber associated with an actor
|
||||
/// </summary>
|
||||
public interface IDispatcherProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the corresponding <see cref="IDispatcher"/>
|
||||
/// </summary>
|
||||
IDispatcher Dispatcher { get; }
|
||||
}
|
||||
}
|
22
Jellyfin.Plugin.MetaShark/Vendor/RateLimiter/AwaitableConstraintExtension.cs
vendored
Normal file
22
Jellyfin.Plugin.MetaShark/Vendor/RateLimiter/AwaitableConstraintExtension.cs
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension to interface <see cref="IAwaitableConstraint"/>
|
||||
/// </summary>
|
||||
public static class AwaitableConstraintExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Compose two awaitable constraint in a new one
|
||||
/// </summary>
|
||||
/// <param name="awaitableConstraint1"></param>
|
||||
/// <param name="awaitableConstraint2"></param>
|
||||
/// <returns></returns>
|
||||
public static IAwaitableConstraint Compose(this IAwaitableConstraint awaitableConstraint1, IAwaitableConstraint awaitableConstraint2)
|
||||
{
|
||||
if (awaitableConstraint1 == awaitableConstraint2)
|
||||
return awaitableConstraint1;
|
||||
|
||||
return new ComposedAwaitableConstraint(awaitableConstraint1, awaitableConstraint2);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
internal class ComposedAwaitableConstraint : IAwaitableConstraint
|
||||
{
|
||||
private readonly IAwaitableConstraint _AwaitableConstraint1;
|
||||
private readonly IAwaitableConstraint _AwaitableConstraint2;
|
||||
private readonly SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
internal ComposedAwaitableConstraint(IAwaitableConstraint awaitableConstraint1, IAwaitableConstraint awaitableConstraint2)
|
||||
{
|
||||
_AwaitableConstraint1 = awaitableConstraint1;
|
||||
_AwaitableConstraint2 = awaitableConstraint2;
|
||||
}
|
||||
|
||||
public IAwaitableConstraint Clone()
|
||||
{
|
||||
return new ComposedAwaitableConstraint(_AwaitableConstraint1.Clone(), _AwaitableConstraint2.Clone());
|
||||
}
|
||||
|
||||
public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
|
||||
{
|
||||
await _Semaphore.WaitAsync(cancellationToken);
|
||||
IDisposable[] disposables;
|
||||
try
|
||||
{
|
||||
disposables = await Task.WhenAll(_AwaitableConstraint1.WaitForReadiness(cancellationToken), _AwaitableConstraint2.WaitForReadiness(cancellationToken));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_Semaphore.Release();
|
||||
throw;
|
||||
}
|
||||
return new DisposeAction(() =>
|
||||
{
|
||||
foreach (var disposable in disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
_Semaphore.Release();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
120
Jellyfin.Plugin.MetaShark/Vendor/RateLimiter/CountByIntervalAwaitableConstraint.cs
vendored
Normal file
120
Jellyfin.Plugin.MetaShark/Vendor/RateLimiter/CountByIntervalAwaitableConstraint.cs
vendored
Normal file
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide an awaitable constraint based on number of times per duration
|
||||
/// </summary>
|
||||
public class CountByIntervalAwaitableConstraint : IAwaitableConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// List of the last time stamps
|
||||
/// </summary>
|
||||
public IReadOnlyList<DateTime> TimeStamps => _TimeStamps.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Stack of the last time stamps
|
||||
/// </summary>
|
||||
protected LimitedSizeStack<DateTime> _TimeStamps { get; }
|
||||
|
||||
private int _Count { get; }
|
||||
private TimeSpan _TimeSpan { get; }
|
||||
private SemaphoreSlim _Semaphore { get; } = new SemaphoreSlim(1, 1);
|
||||
private ITime _Time { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new AwaitableConstraint based on number of times per duration
|
||||
/// </summary>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="timeSpan"></param>
|
||||
public CountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan) : this(count, timeSpan, TimeSystem.StandardTime)
|
||||
{
|
||||
}
|
||||
|
||||
internal CountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan, ITime time)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("count should be strictly positive", nameof(count));
|
||||
|
||||
if (timeSpan.TotalMilliseconds <= 0)
|
||||
throw new ArgumentException("timeSpan should be strictly positive", nameof(timeSpan));
|
||||
|
||||
_Count = count;
|
||||
_TimeSpan = timeSpan;
|
||||
_TimeStamps = new LimitedSizeStack<DateTime>(_Count);
|
||||
_Time = time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns a task that will complete once the constraint is fulfilled
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">
|
||||
/// Cancel the wait
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A disposable that should be disposed upon task completion
|
||||
/// </returns>
|
||||
public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
|
||||
{
|
||||
await _Semaphore.WaitAsync(cancellationToken);
|
||||
var count = 0;
|
||||
var now = _Time.GetNow();
|
||||
var target = now - _TimeSpan;
|
||||
LinkedListNode<DateTime> element = _TimeStamps.First, last = null;
|
||||
while ((element != null) && (element.Value > target))
|
||||
{
|
||||
last = element;
|
||||
element = element.Next;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count < _Count)
|
||||
return new DisposeAction(OnEnded);
|
||||
|
||||
Debug.Assert(element == null);
|
||||
Debug.Assert(last != null);
|
||||
var timeToWait = last.Value.Add(_TimeSpan) - now;
|
||||
try
|
||||
{
|
||||
await _Time.GetDelay(timeToWait, cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_Semaphore.Release();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new DisposeAction(OnEnded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone CountByIntervalAwaitableConstraint
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IAwaitableConstraint Clone()
|
||||
{
|
||||
return new CountByIntervalAwaitableConstraint(_Count, _TimeSpan, _Time);
|
||||
}
|
||||
|
||||
private void OnEnded()
|
||||
{
|
||||
var now = _Time.GetNow();
|
||||
_TimeStamps.Push(now);
|
||||
OnEnded(now);
|
||||
_Semaphore.Release();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when action has been executed
|
||||
/// </summary>
|
||||
/// <param name="now"></param>
|
||||
protected virtual void OnEnded(DateTime now)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
internal class DisposeAction : IDisposable
|
||||
{
|
||||
private Action _Act;
|
||||
|
||||
public DisposeAction(Action act)
|
||||
{
|
||||
_Act = act;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Act?.Invoke();
|
||||
_Act = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a time constraints that can be awaited
|
||||
/// </summary>
|
||||
public interface IAwaitableConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// returns a task that will complete once the constraint is fulfilled
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">
|
||||
/// Cancel the wait
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// A disposable that should be disposed upon task completion
|
||||
/// </returns>
|
||||
Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new IAwaitableConstraint with same constraints but unused
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAwaitableConstraint Clone();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Time abstraction
|
||||
/// </summary>
|
||||
internal interface ITime
|
||||
{
|
||||
/// <summary>
|
||||
/// Return Now DateTime
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
DateTime GetNow();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a task delay
|
||||
/// </summary>
|
||||
/// <param name="timespan"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task GetDelay(TimeSpan timespan, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// LinkedList with a limited size
|
||||
/// If the size exceeds the limit older entry are removed
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class LimitedSizeStack<T>: LinkedList<T>
|
||||
{
|
||||
private readonly int _MaxSize;
|
||||
|
||||
/// <summary>
|
||||
/// Construct the LimitedSizeStack with the given limit
|
||||
/// </summary>
|
||||
/// <param name="maxSize"></param>
|
||||
public LimitedSizeStack(int maxSize)
|
||||
{
|
||||
_MaxSize = maxSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push new entry. If he size exceeds the limit, the oldest entry is removed
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void Push(T item)
|
||||
{
|
||||
AddFirst(item);
|
||||
|
||||
if (Count > _MaxSize)
|
||||
RemoveLast();
|
||||
}
|
||||
}
|
||||
}
|
42
Jellyfin.Plugin.MetaShark/Vendor/RateLimiter/PersistentCountByIntervalAwaitableConstraint.cs
vendored
Normal file
42
Jellyfin.Plugin.MetaShark/Vendor/RateLimiter/PersistentCountByIntervalAwaitableConstraint.cs
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="CountByIntervalAwaitableConstraint"/> that is able to save own state.
|
||||
/// </summary>
|
||||
public sealed class PersistentCountByIntervalAwaitableConstraint : CountByIntervalAwaitableConstraint
|
||||
{
|
||||
private readonly Action<DateTime> _SaveStateAction;
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of <see cref="PersistentCountByIntervalAwaitableConstraint"/>.
|
||||
/// </summary>
|
||||
/// <param name="count">Maximum actions allowed per time interval.</param>
|
||||
/// <param name="timeSpan">Time interval limits are applied for.</param>
|
||||
/// <param name="saveStateAction">Action is used to save state.</param>
|
||||
/// <param name="initialTimeStamps">Initial timestamps.</param>
|
||||
public PersistentCountByIntervalAwaitableConstraint(int count, TimeSpan timeSpan,
|
||||
Action<DateTime> saveStateAction, IEnumerable<DateTime> initialTimeStamps) : base(count, timeSpan)
|
||||
{
|
||||
_SaveStateAction = saveStateAction;
|
||||
|
||||
if (initialTimeStamps == null)
|
||||
return;
|
||||
|
||||
foreach (var timeStamp in initialTimeStamps)
|
||||
{
|
||||
_TimeStamps.Push(timeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save state
|
||||
/// </summary>
|
||||
protected override void OnEnded(DateTime now)
|
||||
{
|
||||
_SaveStateAction(now);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ComposableAsync;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// TimeLimiter implementation
|
||||
/// </summary>
|
||||
public class TimeLimiter : IDispatcher
|
||||
{
|
||||
private readonly IAwaitableConstraint _AwaitableConstraint;
|
||||
|
||||
internal TimeLimiter(IAwaitableConstraint awaitableConstraint)
|
||||
{
|
||||
_AwaitableConstraint = awaitableConstraint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task Enqueue(Func<Task> perform)
|
||||
{
|
||||
return Enqueue(perform, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task<T> Enqueue<T>(Func<Task<T>> perform)
|
||||
{
|
||||
return Enqueue(perform, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Enqueue(Func<Task> perform, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (await _AwaitableConstraint.WaitForReadiness(cancellationToken))
|
||||
{
|
||||
await perform();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> perform, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (await _AwaitableConstraint.WaitForReadiness(cancellationToken))
|
||||
{
|
||||
return await perform();
|
||||
}
|
||||
}
|
||||
|
||||
public IDispatcher Clone() => new TimeLimiter(_AwaitableConstraint.Clone());
|
||||
|
||||
private static Func<Task> Transform(Action act)
|
||||
{
|
||||
return () => { act(); return Task.FromResult(0); };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="compute"></param>
|
||||
/// <returns></returns>
|
||||
private static Func<Task<T>> Transform<T>(Func<T> compute)
|
||||
{
|
||||
return () => Task.FromResult(compute());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task Enqueue(Action perform)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
public void Dispatch(Action action)
|
||||
{
|
||||
Enqueue(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <returns></returns>
|
||||
public Task<T> Enqueue<T>(Func<T> perform)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// returning the result of given function
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task<T> Enqueue<T>(Func<T> perform, CancellationToken cancellationToken)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform the given task respecting the time constraint
|
||||
/// </summary>
|
||||
/// <param name="perform"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public Task Enqueue(Action perform, CancellationToken cancellationToken)
|
||||
{
|
||||
var transformed = Transform(perform);
|
||||
return Enqueue(transformed, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a TimeLimiter based on a maximum number of times
|
||||
/// during a given period
|
||||
/// </summary>
|
||||
/// <param name="maxCount"></param>
|
||||
/// <param name="timeSpan"></param>
|
||||
/// <returns></returns>
|
||||
public static TimeLimiter GetFromMaxCountByInterval(int maxCount, TimeSpan timeSpan)
|
||||
{
|
||||
return new TimeLimiter(new CountByIntervalAwaitableConstraint(maxCount, timeSpan));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create <see cref="TimeLimiter"/> that will save state using action passed through <paramref name="saveStateAction"/> parameter.
|
||||
/// </summary>
|
||||
/// <param name="maxCount">Maximum actions allowed per time interval.</param>
|
||||
/// <param name="timeSpan">Time interval limits are applied for.</param>
|
||||
/// <param name="saveStateAction">Action is used to save state.</param>
|
||||
/// <returns><see cref="TimeLimiter"/> instance with <see cref="PersistentCountByIntervalAwaitableConstraint"/>.</returns>
|
||||
public static TimeLimiter GetPersistentTimeLimiter(int maxCount, TimeSpan timeSpan,
|
||||
Action<DateTime> saveStateAction)
|
||||
{
|
||||
return GetPersistentTimeLimiter(maxCount, timeSpan, saveStateAction, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create <see cref="TimeLimiter"/> with initial timestamps that will save state using action passed through <paramref name="saveStateAction"/> parameter.
|
||||
/// </summary>
|
||||
/// <param name="maxCount">Maximum actions allowed per time interval.</param>
|
||||
/// <param name="timeSpan">Time interval limits are applied for.</param>
|
||||
/// <param name="saveStateAction">Action is used to save state.</param>
|
||||
/// <param name="initialTimeStamps">Initial timestamps.</param>
|
||||
/// <returns><see cref="TimeLimiter"/> instance with <see cref="PersistentCountByIntervalAwaitableConstraint"/>.</returns>
|
||||
public static TimeLimiter GetPersistentTimeLimiter(int maxCount, TimeSpan timeSpan,
|
||||
Action<DateTime> saveStateAction, IEnumerable<DateTime> initialTimeStamps)
|
||||
{
|
||||
return new TimeLimiter(new PersistentCountByIntervalAwaitableConstraint(maxCount, timeSpan, saveStateAction, initialTimeStamps));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compose various IAwaitableConstraint in a TimeLimiter
|
||||
/// </summary>
|
||||
/// <param name="constraints"></param>
|
||||
/// <returns></returns>
|
||||
public static TimeLimiter Compose(params IAwaitableConstraint[] constraints)
|
||||
{
|
||||
var composed = constraints.Aggregate(default(IAwaitableConstraint),
|
||||
(accumulated, current) => (accumulated == null) ? current : accumulated.Compose(current));
|
||||
return new TimeLimiter(composed);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RateLimiter
|
||||
{
|
||||
internal class TimeSystem : ITime
|
||||
{
|
||||
public static ITime StandardTime { get; }
|
||||
|
||||
static TimeSystem()
|
||||
{
|
||||
StandardTime = new TimeSystem();
|
||||
}
|
||||
|
||||
private TimeSystem()
|
||||
{
|
||||
}
|
||||
|
||||
DateTime ITime.GetNow()
|
||||
{
|
||||
return DateTime.Now;
|
||||
}
|
||||
|
||||
Task ITime.GetDelay(TimeSpan timespan, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Delay(timespan, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ jellyfin电影元数据插件,影片信息只要从豆瓣获取,并由TheMov
|
|||
3. 识别时默认不返回TheMovieDb结果,有需要可以到插件配置中打开
|
||||
4. 假如网络原因访问TheMovieDb比较慢,可以到插件配置中关闭从TheMovieDb获取数据(关闭后不会再获取剧集信息)
|
||||
|
||||
> 🚨建议一次不要刮削太多电影,要不然会触发豆瓣风控,导致被封IP(封IP需要等6小时左右才能恢复访问)
|
||||
> 🚨假如需要刮削大量电影,请到插件配置中打开防封禁功能,避免频繁请求豆瓣导致被封IP(封IP需要等6小时左右才能恢复访问)
|
||||
|
||||
## How to build
|
||||
|
||||
|
|
Loading…
Reference in New Issue