feat: add douban rate limit #12

This commit is contained in:
cxfksword 2023-01-13 20:34:26 +08:00
parent eede72d771
commit c79bfc5bd1
42 changed files with 1715 additions and 93 deletions

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ artifacts
**/.DS_Store **/.DS_Store
metashark/ metashark/
manifest_cn.json manifest_cn.json
manifest.json manifest.json
.vscode

View File

@ -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] [TestMethod]
public void TestGetVideoBySidAsync() public void TestGetVideoBySidAsync()
{ {

View File

@ -50,5 +50,29 @@ namespace Jellyfin.Plugin.MetaShark.Test
}).GetAwaiter().GetResult(); }).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();
}
} }
} }

View File

@ -31,6 +31,8 @@ using Jellyfin.Plugin.MetaShark.Core;
using System.Data; using System.Data;
using TMDbLib.Objects.Movies; using TMDbLib.Objects.Movies;
using System.Xml.Linq; using System.Xml.Linq;
using RateLimiter;
using ComposableAsync;
namespace Jellyfin.Plugin.MetaShark.Api namespace Jellyfin.Plugin.MetaShark.Api
{ {
@ -50,6 +52,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
Regex regSid = new Regex(@"sid: (\d+?),", RegexOptions.Compiled); Regex regSid = new Regex(@"sid: (\d+?),", RegexOptions.Compiled);
Regex regCat = new Regex(@"\[(.+?)\]", RegexOptions.Compiled); Regex regCat = new Regex(@"\[(.+?)\]", RegexOptions.Compiled);
Regex regYear = new Regex(@"(\d{4})", 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 regOriginalName = new Regex(@"原名[:](.+?)\s*?\/", RegexOptions.Compiled);
Regex regDirector = new Regex(@"导演: (.+?)\n", RegexOptions.Compiled); Regex regDirector = new Regex(@"导演: (.+?)\n", RegexOptions.Compiled);
Regex regWriter = 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 regFamily = new Regex(@"家庭成员: \n(.+?)\n", RegexOptions.Compiled);
Regex regCelebrityImdb = new Regex(@"imdb编号:\s+?(nm\d+)", 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> /// <summary>
/// Initializes a new instance of the <see cref="DoubanApi"/> class. /// Initializes a new instance of the <see cref="DoubanApi"/> class.
/// </summary> /// </summary>
@ -86,9 +98,9 @@ namespace Jellyfin.Plugin.MetaShark.Api
var handler = new HttpClientHandlerEx(); var handler = new HttpClientHandlerEx();
this._cookieContainer = handler.CookieContainer; this._cookieContainer = handler.CookieContainer;
httpClient = new HttpClient(handler, true); httpClient = new HttpClient(handler);
httpClient.Timeout = TimeSpan.FromSeconds(10); 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("Origin", "https://movie.douban.com");
httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/"); httpClient.DefaultRequestHeaders.Add("Referer", "https://movie.douban.com/");
} }
@ -162,7 +174,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
EnsureLoadDoubanCookie(); EnsureLoadDoubanCookie();
// LimitRequestFrequently(2000); await LimitRequestFrequently();
var encodedKeyword = HttpUtility.UrlEncode(keyword); var encodedKeyword = HttpUtility.UrlEncode(keyword);
var url = $"https://www.douban.com/search?cat=1002&q={encodedKeyword}"; var url = $"https://www.douban.com/search?cat=1002&q={encodedKeyword}";
@ -220,6 +232,56 @@ namespace Jellyfin.Plugin.MetaShark.Api
return list; 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) public async Task<DoubanSubject?> GetMovieAsync(string sid, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(sid)) if (string.IsNullOrEmpty(sid))
@ -236,7 +298,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
} }
EnsureLoadDoubanCookie(); EnsureLoadDoubanCookie();
// LimitRequestFrequently(); await LimitRequestFrequently();
var url = $"https://movie.douban.com/subject/{sid}/"; var url = $"https://movie.douban.com/subject/{sid}/";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
@ -253,14 +315,8 @@ namespace Jellyfin.Plugin.MetaShark.Api
if (contentNode != null) if (contentNode != null)
{ {
var nameStr = contentNode.GetText("h1>span:first-child") ?? string.Empty; var nameStr = contentNode.GetText("h1>span:first-child") ?? string.Empty;
var match = this.regNameMath.Match(nameStr); var name = GetTitle(body);
var name = string.Empty; var orginalName = nameStr.Replace(name, "").Trim();
var orginalName = string.Empty;
if (match.Success && match.Groups.Count == 3)
{
name = match.Groups[1].Value;
orginalName = match.Groups[2].Value;
}
var yearStr = contentNode.GetText("h1>span.year") ?? string.Empty; var yearStr = contentNode.GetText("h1>span.year") ?? string.Empty;
var year = yearStr.GetMatchGroup(this.regYear); var year = yearStr.GetMatchGroup(this.regYear);
var rating = contentNode.GetText("div.rating_self strong.rating_num") ?? "0"; var rating = contentNode.GetText("div.rating_self strong.rating_num") ?? "0";
@ -347,7 +403,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
} }
EnsureLoadDoubanCookie(); EnsureLoadDoubanCookie();
// LimitRequestFrequently(); await LimitRequestFrequently();
var list = new List<DoubanCelebrity>(); var list = new List<DoubanCelebrity>();
var url = $"https://movie.douban.com/subject/{sid}/celebrities"; var url = $"https://movie.douban.com/subject/{sid}/celebrities";
@ -493,7 +549,6 @@ namespace Jellyfin.Plugin.MetaShark.Api
EnsureLoadDoubanCookie(); EnsureLoadDoubanCookie();
// LimitRequestFrequently();
keyword = HttpUtility.UrlEncode(keyword); keyword = HttpUtility.UrlEncode(keyword);
@ -547,7 +602,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
} }
EnsureLoadDoubanCookie(); EnsureLoadDoubanCookie();
// LimitRequestFrequently(); await LimitRequestFrequently();
var list = new List<DoubanPhoto>(); var list = new List<DoubanPhoto>();
var url = $"https://movie.douban.com/subject/{sid}/photos?type=W&start=0&sortby=size&size=a&subtype=a"; 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; return list;
} }
public async Task<bool> CheckLoginAsync(CancellationToken cancellationToken)
protected void LimitRequestFrequently(int interval = 1000)
{ {
var diff = 0; EnsureLoadDoubanCookie();
lock (_lock)
{
var ts = DateTime.Now - lastRequestTime;
diff = (int)(interval - ts.TotalMilliseconds);
if (diff > 0)
{
this._logger.LogInformation("请求太频繁,等待{0}毫秒后继续执行...", diff);
Thread.Sleep(diff);
}
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) private string? GetText(IElement el, string css)
{ {
@ -650,6 +786,11 @@ namespace Jellyfin.Plugin.MetaShark.Api
return string.Empty; return string.Empty;
} }
private bool IsEnableAvoidRiskControl()
{
return Plugin.Instance?.Configuration.EnableDoubanAvoidRiskControl ?? false;
}
public void Dispose() public void Dispose()
{ {
@ -661,6 +802,7 @@ namespace Jellyfin.Plugin.MetaShark.Api
{ {
if (disposing) if (disposing)
{ {
httpClient.Dispose();
_memoryCache.Dispose(); _memoryCache.Dispose();
} }
} }

View File

@ -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);
}
}
}

View File

@ -25,7 +25,12 @@ public enum SomeOptions
/// </summary> /// </summary>
public class PluginConfiguration : BasePluginConfiguration 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 EnableTmdb { get; set; } = true;
public bool EnableTmdbSearch { get; set; } = false; public bool EnableTmdbSearch { get; set; } = false;
@ -34,7 +39,7 @@ public class PluginConfiguration : BasePluginConfiguration
public string TmdbHost { get; set; } = string.Empty; public string TmdbHost { get; set; } = string.Empty;
public string DoubanCookies { get; set; } = string.Empty;
public int MaxCastMembers { get; set; } = 15; public int MaxCastMembers { get; set; } = 15;

View File

@ -22,12 +22,33 @@
</div> </div>
<form id="TemplateConfigForm"> <form id="TemplateConfigForm">
<div class="inputContainer"> <fieldset class="verticalSection verticalSection-extrabottompadding">
<label class="inputLabel inputLabelUnfocused" for="DoubanCookies">豆瓣网站cookie</label> <legend>
<textarea rows="5" is="emby-input" type="text" id="DoubanCookies" name="DoubanCookies" <h3>豆瓣</h3>
class="emby-input" placeholder="_vwo_uuid_v2=1; __utmv=2; ..."></textarea> </legend>
<div class="fieldDescription">可为空,填写可搜索到需登录访问的影片,使用分号“;”分隔格式cookie.</div> <div class="inputContainer">
</div> <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"> <fieldset class="verticalSection verticalSection-extrabottompadding">
<legend> <legend>
@ -80,10 +101,14 @@
$('#current_version').text("v" + config.Version); $('#current_version').text("v" + config.Version);
document.querySelector('#DoubanCookies').value = config.DoubanCookies; document.querySelector('#DoubanCookies').value = config.DoubanCookies;
document.querySelector('#EnableDoubanAvoidRiskControl').checked = config.EnableDoubanAvoidRiskControl;
document.querySelector('#EnableTmdb').checked = config.EnableTmdb; document.querySelector('#EnableTmdb').checked = config.EnableTmdb;
document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch; document.querySelector('#EnableTmdbSearch').checked = config.EnableTmdbSearch;
document.querySelector('#TmdbApiKey').value = config.TmdbApiKey; document.querySelector('#TmdbApiKey').value = config.TmdbApiKey;
document.querySelector('#TmdbHost').value = config.TmdbHost; document.querySelector('#TmdbHost').value = config.TmdbHost;
checkDoubanLogin();
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
}); });
@ -93,18 +118,38 @@
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(TemplateConfig.pluginUniqueId).then(function (config) {
config.DoubanCookies = document.querySelector('#DoubanCookies').value; config.DoubanCookies = document.querySelector('#DoubanCookies').value;
config.EnableDoubanAvoidRiskControl = document.querySelector('#EnableDoubanAvoidRiskControl').checked;
config.EnableTmdb = document.querySelector('#EnableTmdb').checked; config.EnableTmdb = document.querySelector('#EnableTmdb').checked;
config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked; config.EnableTmdbSearch = document.querySelector('#EnableTmdbSearch').checked;
config.TmdbApiKey = document.querySelector('#TmdbApiKey').value; config.TmdbApiKey = document.querySelector('#TmdbApiKey').value;
config.TmdbHost = document.querySelector('#TmdbHost').value; config.TmdbHost = document.querySelector('#TmdbHost').value;
ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) { ApiClient.updatePluginConfiguration(TemplateConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result); Dashboard.processPluginConfigurationUpdateResult(result);
checkDoubanLogin();
}); });
}); });
e.preventDefault(); e.preventDefault();
return false; 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> </script>
</div> </div>
</body> </body>

View File

@ -1,3 +1,4 @@
using System.Threading;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -19,6 +20,8 @@ using System.Runtime.InteropServices;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using Jellyfin.Plugin.MetaShark.Api;
using Jellyfin.Plugin.MetaShark.Model;
namespace Jellyfin.Plugin.MetaShark.Controllers namespace Jellyfin.Plugin.MetaShark.Controllers
{ {
@ -27,15 +30,17 @@ namespace Jellyfin.Plugin.MetaShark.Controllers
[Route("/plugin/metashark")] [Route("/plugin/metashark")]
public class MetaSharkController : ControllerBase public class MetaSharkController : ControllerBase
{ {
private readonly DoubanApi _doubanApi;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MetaSharkController"/> class. /// Initializes a new instance of the <see cref="MetaSharkController"/> class.
/// </summary> /// </summary>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param> /// <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; 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() private HttpClient GetHttpClient()
{ {
var client = _httpClientFactory.CreateClient(NamedClient.Default); var client = _httpClientFactory.CreateClient(NamedClient.Default);

View File

@ -75,7 +75,7 @@ namespace Jellyfin.Plugin.MetaShark.Core
} }
// 假如Anitomy解析不到year尝试使用jellyfin默认parser看能不能解析成功 // 假如Anitomy解析不到year尝试使用jellyfin默认parser看能不能解析成功
if (parseResult.Year == null) if (parseResult.Year == null && !IsAnime(fileName))
{ {
var nativeParseResult = ParseMovie(fileName); var nativeParseResult = ParseMovie(fileName);
if (nativeParseResult.Year != null) if (nativeParseResult.Year != null)
@ -165,6 +165,11 @@ namespace Jellyfin.Plugin.MetaShark.Core
return true; return true;
} }
if (Regex.Match(name, @"\[.+\].*?\[.+?\]", RegexOptions.IgnoreCase).Success)
{
return true;
}
return false; return false;
} }
} }

View File

@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace> <RootNamespace>Jellyfin.Plugin.MetaShark</RootNamespace>
@ -8,35 +7,28 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode> <AnalysisMode>AllEnabledByDefault</AnalysisMode>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>False</TreatWarningsAsErrors> <TreatWarningsAsErrors>False</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>False</TreatWarningsAsErrors> <TreatWarningsAsErrors>False</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="0.17.1" /> <PackageReference Include="AngleSharp" Version="0.17.1" />
<PackageReference Include="AnitomySharp" Version="0.2.0" /> <PackageReference Include="AnitomySharp" Version="0.2.0" />
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" /> <PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
<PackageReference Include="Jellyfin.Model" Version="10.8.0" /> <PackageReference Include="Jellyfin.Model" Version="10.8.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Configuration\configPage.html" /> <None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" /> <EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Vendor\TMDbLib\TMDbLib.csproj" /> <None Include="Vendor\TMDbLib\TMDbLib.csproj" />
</ItemGroup> </ItemGroup>
</Project>
</Project>

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}

View File

@ -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}"); 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); List<DoubanSubject> result;
var jw = new JaroWinkler(); DoubanSubject? item;
foreach (var item in result)
// 假如存在年份先通过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; this.Log($"GuessByDouban of [name]: {searchName} found Sid: {item.Sid}");
}
// 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}");
return 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; return null;
@ -125,24 +140,35 @@ namespace Jellyfin.Plugin.MetaShark.Providers
} }
this.Log($"GuestDoubanSeasonByYear of [name]: {name} [year]: {year}"); this.Log($"GuestDoubanSeasonByYear of [name]: {name} [year]: {year}");
var result = await this._doubanApi.SearchAsync(name, cancellationToken).ConfigureAwait(false);
var jw = new JaroWinkler(); // 先通过suggest接口查找减少搜索页访问次数避免封禁suggest没法区分电影或电视剧排序也比搜索页差些
foreach (var item in result) 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;
} }
suggestItem = suggestResult.Where(x => x.Year == year).FirstOrDefault();
// bt种子都是英文名但电影是中日韩泰印法地区时都不适用相似匹配去掉限制 if (suggestItem != null)
if (year == item.Year)
{ {
this.Log($"GuestDoubanSeasonByYear of [name] found Sid: \"{item.Sid}\""); this.Log($"GuestDoubanSeasonByYear of [name] found Sid: \"{suggestItem.Sid}\" (suggest)");
return item.Sid; 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; 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}"); this.Log($"GuestByTmdb of [name]: {info.Name} [file_name]: {fileName} [year]: {info.Year} [search name]: {searchName}");
var jw = new JaroWinkler();
switch (info) switch (info)
{ {
case MovieInfo: case MovieInfo:
var movieResults = await this._tmdbApi.SearchMovieAsync(searchName, info.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); 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种子都是英文名但电影是中日韩泰印法地区时都不适用相似匹配去掉限制 // bt种子都是英文名但电影是中日韩泰印法地区时都不适用相似匹配去掉限制
return item.Id.ToString(CultureInfo.InvariantCulture); return movieItem.Id.ToString(CultureInfo.InvariantCulture);
} }
break; break;
case SeriesInfo: case SeriesInfo:
var seriesResults = await this._tmdbApi.SearchSeriesAsync(searchName, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); 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种子都是英文名但电影是中日韩泰印法地区时都不适用相似匹配去掉限制 // bt种子都是英文名但电影是中日韩泰印法地区时都不适用相似匹配去掉限制
return item.Id.ToString(CultureInfo.InvariantCulture); return seriesItem.Id.ToString(CultureInfo.InvariantCulture);
} }
break; break;
} }

View File

@ -58,7 +58,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid)) if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{ {
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken); var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
if (primary == null) if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
{ {
return Enumerable.Empty<RemoteImageInfo>(); return Enumerable.Empty<RemoteImageInfo>();
} }

View File

@ -58,7 +58,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid)) if (metaSource != MetaSource.Tmdb && !string.IsNullOrEmpty(sid))
{ {
var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken); var primary = await this._doubanApi.GetMovieAsync(sid, cancellationToken);
if (primary == null) if (primary == null || string.IsNullOrEmpty(primary.ImgMiddle))
{ {
return Enumerable.Empty<RemoteImageInfo>(); return Enumerable.Empty<RemoteImageInfo>();
} }
@ -152,7 +152,7 @@ namespace Jellyfin.Plugin.MetaShark.Providers
return list; 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 return new RemoteImageInfo
{ {

View File

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ComposableAsync.Core.Test")]

View 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() { }
}
}

View File

@ -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);
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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();
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View 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; }
}
}

View 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);
}
}
}

View File

@ -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();
});
}
}
}

View 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)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -32,7 +32,7 @@ jellyfin电影元数据插件影片信息只要从豆瓣获取并由TheMov
3. 识别时默认不返回TheMovieDb结果有需要可以到插件配置中打开 3. 识别时默认不返回TheMovieDb结果有需要可以到插件配置中打开
4. 假如网络原因访问TheMovieDb比较慢可以到插件配置中关闭从TheMovieDb获取数据关闭后不会再获取剧集信息 4. 假如网络原因访问TheMovieDb比较慢可以到插件配置中关闭从TheMovieDb获取数据关闭后不会再获取剧集信息
> 🚨建议一次不要刮削太多电影,要不然会触发豆瓣风控,导致被封IP封IP需要等6小时左右才能恢复访问 > 🚨假如需要刮削大量电影,请到插件配置中打开防封禁功能,避免频繁请求豆瓣导致被封IP封IP需要等6小时左右才能恢复访问
## How to build ## How to build