mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-04-24 02:22:09 +08:00
Add youku danmu
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Jellyfin.Plugin.Danmu/Vendor/** linguist-vendored
|
||||
@@ -1,23 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jellyfin.Plugin.Danmu\Jellyfin.Plugin.Danmu.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jellyfin.Plugin.Danmu\Jellyfin.Plugin.Danmu.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
107
Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs
Normal file
107
Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Model;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Youku;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Test
|
||||
{
|
||||
|
||||
[TestClass]
|
||||
public class YoukuApiTest
|
||||
{
|
||||
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
|
||||
builder.AddSimpleConsole(options =>
|
||||
{
|
||||
options.IncludeScopes = true;
|
||||
options.SingleLine = true;
|
||||
options.TimestampFormat = "hh:mm:ss ";
|
||||
}));
|
||||
|
||||
[TestMethod]
|
||||
public void TestSearch()
|
||||
{
|
||||
var keyword = "夏洛特";
|
||||
var api = new YoukuApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await api.SearchAsync(keyword, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetVideo()
|
||||
{
|
||||
var api = new YoukuApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var vid = "0b39c5b6569311e5b2ad";
|
||||
var result = await api.GetVideoAsync(vid, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetDanmuContentByMat()
|
||||
{
|
||||
var api = new YoukuApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var vid = "XMTM1MTc4MDU3Ng==";
|
||||
var result = await api.GetDanmuContentByMatAsync(vid, 0, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestGetDanmu()
|
||||
{
|
||||
var api = new YoukuApi(loggerFactory);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var vid = "XMTM1MTc4MDU3Ng==";
|
||||
var result = await api.GetDanmuContentAsync(vid, CancellationToken.None);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
58
Jellyfin.Plugin.Danmu/Core/Extensions/ElementExtension.cs
Normal file
58
Jellyfin.Plugin.Danmu/Core/Extensions/ElementExtension.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// using AngleSharp.Dom;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
||||
{
|
||||
public static class ElementExtension
|
||||
{
|
||||
// public static string? GetText(this IElement el, string css)
|
||||
// {
|
||||
// var node = el.QuerySelector(css);
|
||||
// if (node != null)
|
||||
// {
|
||||
// return node.Text().Trim();
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// public static string GetTextOrDefault(this IElement el, string css, string defaultVal = "")
|
||||
// {
|
||||
// var node = el.QuerySelector(css);
|
||||
// if (node != null)
|
||||
// {
|
||||
// return node.Text().Trim();
|
||||
// }
|
||||
|
||||
// return defaultVal;
|
||||
// }
|
||||
|
||||
// public static string? GetAttr(this IElement el, string css, string attr)
|
||||
// {
|
||||
// var node = el.QuerySelector(css);
|
||||
// if (node != null)
|
||||
// {
|
||||
// var attrVal = node.GetAttribute(attr);
|
||||
// return attrVal != null ? attrVal.Trim() : null;
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// public static string? GetAttrOrDefault(this IElement el, string css, string attr, string defaultVal = "")
|
||||
// {
|
||||
// var node = el.QuerySelector(css);
|
||||
// if (node != null)
|
||||
// {
|
||||
// var attrVal = node.GetAttribute(attr);
|
||||
// return attrVal != null ? attrVal.Trim() : defaultVal;
|
||||
// }
|
||||
|
||||
// return defaultVal;
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,10 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
||||
{
|
||||
if (obj == null) return string.Empty;
|
||||
|
||||
return JsonSerializer.Serialize(obj);
|
||||
// 不指定UnsafeRelaxedJsonEscaping,+号会被转码为unicode字符,和js/java的序列化不一致
|
||||
var jso = new JsonSerializerOptions();
|
||||
jso.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
|
||||
return JsonSerializer.Serialize(obj, jso);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
Jellyfin.Plugin.Danmu/Core/Extensions/RegexExtension.cs
Normal file
35
Jellyfin.Plugin.Danmu/Core/Extensions/RegexExtension.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
||||
{
|
||||
public static class RegexExtension
|
||||
{
|
||||
public static string FirstMatch(this Regex reg, string text, string defaultVal = "")
|
||||
{
|
||||
var match = reg.Match(text);
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
public static string FirstMatchGroup(this Regex reg, string text, string defaultVal = "")
|
||||
{
|
||||
var match = reg.Match(text);
|
||||
if (match.Success && match.Groups.Count > 1)
|
||||
{
|
||||
return match.Groups[1].Value.Trim();
|
||||
}
|
||||
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,36 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static double ToDouble(this string s)
|
||||
{
|
||||
double val;
|
||||
if (double.TryParse(s, out val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public static string ToMD5(this string str)
|
||||
{
|
||||
using (var cryptoMD5 = System.Security.Cryptography.MD5.Create())
|
||||
{
|
||||
//將字串編碼成 UTF8 位元組陣列
|
||||
var bytes = Encoding.UTF8.GetBytes(str);
|
||||
|
||||
//取得雜湊值位元組陣列
|
||||
var hash = cryptoMD5.ComputeHash(bytes);
|
||||
|
||||
//取得 MD5
|
||||
var md5 = BitConverter.ToString(hash)
|
||||
.Replace("-", String.Empty)
|
||||
.ToUpper();
|
||||
|
||||
return md5;
|
||||
}
|
||||
}
|
||||
|
||||
public static double Distance(this string s1, string s2)
|
||||
{
|
||||
var jw = new JaroWinkler();
|
||||
|
||||
84
Jellyfin.Plugin.Danmu/Core/Http/HttpLoggingHandler.cs
Normal file
84
Jellyfin.Plugin.Danmu/Core/Http/HttpLoggingHandler.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
// ReSharper disable TemplateIsNotCompileTimeConstantProblem
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Http
|
||||
{
|
||||
[SuppressMessage("Usage", "CA2254:模板应为静态表达式", Justification = "<挂起>")]
|
||||
public class HttpLoggingHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public HttpLoggingHandler(HttpMessageHandler innerHandler, ILogger logger)
|
||||
: base(innerHandler)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
HttpResponseMessage? response = null;
|
||||
try
|
||||
{
|
||||
response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await LogRequestAndResponse(request, response, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
await LogRequestAndResponse(request, response, exception, cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LogRequestAndResponse(HttpRequestMessage? request, HttpResponseMessage? response, Exception? exception, CancellationToken cancellationToken)
|
||||
{
|
||||
var log = new StringBuilder();
|
||||
try
|
||||
{
|
||||
if (request != null)
|
||||
{
|
||||
log.Append("Request: ").Append(request).Append('\n');
|
||||
}
|
||||
|
||||
if (request?.Content != null)
|
||||
{
|
||||
log.Append("Content: ").Append(await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)).Append('\n');
|
||||
}
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
log.Append("Response: ").Append(response).Append('\n');
|
||||
}
|
||||
|
||||
if (response?.Content != null)
|
||||
{
|
||||
log.Append("Content: ").Append(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)).Append('\n');
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
_logger.LogWarning(log.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(exception, log.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Jellyfin.Plugin.Danmu/Core/Http/HttpRetryMessageHandler.cs
Normal file
19
Jellyfin.Plugin.Danmu/Core/Http/HttpRetryMessageHandler.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Core.Http
|
||||
{
|
||||
public class HttpRetryMessageHandler : DelegatingHandler
|
||||
{
|
||||
public HttpRetryMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken) =>
|
||||
base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,6 @@ public class DandanApi : AbstractApi
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var ddd = response.Content.ToString();
|
||||
var result = await response.Content.ReadFromJsonAsync<AnimeResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && result.Success)
|
||||
{
|
||||
|
||||
52
Jellyfin.Plugin.Danmu/Scrapers/Youku/Entity/YoukuComment.cs
Normal file
52
Jellyfin.Plugin.Danmu/Scrapers/Youku/Entity/YoukuComment.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuComment
|
||||
{
|
||||
// "mat": 22,
|
||||
// "createtime": 1673844600000,
|
||||
// "ver": 1,
|
||||
// "propertis": "{\"size\":2,\"color\":16524894,\"pos\":3,\"alpha\":1}",
|
||||
// "iid": "XMTM1MTc4MDU3Ng==",
|
||||
// "level": 0,
|
||||
// "lid": 0,
|
||||
// "type": 1,
|
||||
// "content": "打自己一拳呗",
|
||||
// "extFields": {
|
||||
// "grade": 3,
|
||||
// "voteUp": 0
|
||||
// },
|
||||
// "ct": 10004,
|
||||
// "uid": "UNTYzMTg5NzAzNg==",
|
||||
// "uid2": "1407974259",
|
||||
// "ouid": "UMTM2OTY2ODYwMA==",
|
||||
// "playat": 1320797,
|
||||
// "id": 5559881890,
|
||||
// "aid": 300707,
|
||||
// "status": 99
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public Int64 ID { get; set; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; }
|
||||
|
||||
// 毫秒
|
||||
[JsonPropertyName("playat")]
|
||||
public Int64 Playat { get; set; }
|
||||
|
||||
// "{\"size\":2,\"color\":16524894,\"pos\":3,\"alpha\":1}",
|
||||
[JsonPropertyName("propertis")]
|
||||
public string Propertis { get; set; }
|
||||
|
||||
[JsonPropertyName("uid")]
|
||||
public string Uid { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuCommentProperty
|
||||
{
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int Size { get; set; }
|
||||
|
||||
[JsonPropertyName("color")]
|
||||
public uint Color { get; set; }
|
||||
|
||||
[JsonPropertyName("pos")]
|
||||
public int Pos { get; set; }
|
||||
|
||||
[JsonPropertyName("alpha")]
|
||||
public int Alpha { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuCommentResult
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public YoukuCommentData Data { get; set; }
|
||||
}
|
||||
|
||||
public class YoukuCommentData
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
public List<YoukuComment> Result { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
39
Jellyfin.Plugin.Danmu/Scrapers/Youku/Entity/YoukuEpisode.cs
Normal file
39
Jellyfin.Plugin.Danmu/Scrapers/Youku/Entity/YoukuEpisode.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuEpisode
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string ID { get; set; }
|
||||
|
||||
[JsonPropertyName("seq")]
|
||||
public string Seq { get; set; }
|
||||
|
||||
[JsonPropertyName("duration")]
|
||||
public string Duration { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("link")]
|
||||
public string Link { get; set; }
|
||||
|
||||
|
||||
public int TotalMat
|
||||
{
|
||||
get
|
||||
{
|
||||
var duration = Duration.ToDouble();
|
||||
return (int)Math.Floor(duration / 60) + 1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuRpcResult
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public YoukuRpcData Data { get; set; }
|
||||
}
|
||||
|
||||
public class YoukuRpcData
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
public string Result { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuTrackInfo
|
||||
{
|
||||
[JsonPropertyName("group_id")]
|
||||
public string GroupID { get; set; }
|
||||
|
||||
[JsonPropertyName("object_type")]
|
||||
public int ObjectType { get; set; }
|
||||
|
||||
[JsonPropertyName("object_title")]
|
||||
public string ObjectTitle { get; set; }
|
||||
|
||||
[JsonPropertyName("object_url")]
|
||||
public string ObjectUrl { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int? Year { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Type { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
19
Jellyfin.Plugin.Danmu/Scrapers/Youku/Entity/YoukuVideo.cs
Normal file
19
Jellyfin.Plugin.Danmu/Scrapers/Youku/Entity/YoukuVideo.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity
|
||||
{
|
||||
public class YoukuVideo
|
||||
{
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; set; }
|
||||
|
||||
[JsonPropertyName("videos")]
|
||||
public List<YoukuEpisode> Videos { get; set; } = new List<YoukuEpisode>();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.ExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class EpisodeExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Youku.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Youku.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://v.youku.com/v_show/id_{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Episode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.ExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class MovieExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Youku.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Youku.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://v.youku.com/v_show/id_{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Movie;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku.ExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SeasonExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Youku.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Youku.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://v.youku.com/v_nextstage/id_{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Season;
|
||||
}
|
||||
|
||||
}
|
||||
170
Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs
Normal file
170
Jellyfin.Plugin.Danmu/Scrapers/Youku/Youku.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.Danmu.Core;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
using System.Text.Json;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku;
|
||||
|
||||
public class Youku : AbstractScraper
|
||||
{
|
||||
public const string ScraperProviderName = "优酷";
|
||||
public const string ScraperProviderId = "YoukuID";
|
||||
|
||||
private readonly YoukuApi _api;
|
||||
|
||||
public Youku(ILoggerFactory logManager)
|
||||
: base(logManager.CreateLogger<Youku>())
|
||||
{
|
||||
_api = new YoukuApi(logManager);
|
||||
}
|
||||
|
||||
public override int DefaultOrder => 3;
|
||||
|
||||
public override bool DefaultEnable => false;
|
||||
|
||||
public override string Name => "优酷";
|
||||
|
||||
public override string ProviderName => ScraperProviderName;
|
||||
|
||||
public override string ProviderId => ScraperProviderId;
|
||||
|
||||
public override async Task<string?> GetMatchMediaId(BaseItem item)
|
||||
{
|
||||
var searchName = this.NormalizeSearchName(item.Name);
|
||||
var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false);
|
||||
foreach (var video in videos)
|
||||
{
|
||||
var videoId = video.GroupID;
|
||||
var title = video.ObjectTitle;
|
||||
var pubYear = video.Year;
|
||||
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
|
||||
if (isMovieItemType && video.Type != "movie")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMovieItemType && video.Type == "movie")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检测标题是否相似(越大越相似)
|
||||
var score = searchName.Distance(title);
|
||||
if (score < 0.7)
|
||||
{
|
||||
log.LogInformation("[{0}] 标题差异太大,忽略处理. 搜索词:{1}, score: {2}", title, searchName, score);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检测年份是否一致
|
||||
var itemPubYear = item.ProductionYear ?? 0;
|
||||
if (itemPubYear > 0 && pubYear > 0 && itemPubYear != pubYear)
|
||||
{
|
||||
log.LogInformation("[{0}] 发行年份不一致,忽略处理. Youku:{1} jellyfin: {2}", title, pubYear, itemPubYear);
|
||||
continue;
|
||||
}
|
||||
|
||||
return $"{videoId}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public override async Task<ScraperMedia?> GetMedia(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var video = await _api.GetVideoAsync(id, CancellationToken.None).ConfigureAwait(false);
|
||||
if (video == null)
|
||||
{
|
||||
log.LogInformation("[{0}]获取不到视频信息:id={1}", this.Name, id);
|
||||
return null;
|
||||
}
|
||||
|
||||
var media = new ScraperMedia();
|
||||
media.Id = id;
|
||||
media.Name = "no name";
|
||||
if (video.Videos != null && video.Videos.Count > 0)
|
||||
{
|
||||
foreach (var item in video.Videos)
|
||||
{
|
||||
media.Episodes.Add(new ScraperEpisode() { Id = $"{item.ID}", CommentId = $"{item.ID}" });
|
||||
}
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
public override async Task<ScraperEpisode?> GetMediaEpisode(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScraperEpisode() { Id = id, CommentId = id };
|
||||
}
|
||||
|
||||
public override async Task<ScraperDanmaku?> GetDanmuContent(string commentId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commentId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var comments = await _api.GetDanmuContentAsync(commentId, CancellationToken.None).ConfigureAwait(false);
|
||||
var danmaku = new ScraperDanmaku();
|
||||
danmaku.ChatId = 1000;
|
||||
danmaku.ChatServer = "acs.youku.com";
|
||||
foreach (var item in comments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var danmakuText = new ScraperDanmakuText();
|
||||
danmakuText.Progress = (int)item.Playat;
|
||||
danmakuText.Mode = 1;
|
||||
danmakuText.MidHash = item.Uid;
|
||||
danmakuText.Id = item.ID;
|
||||
danmakuText.Content = item.Content;
|
||||
|
||||
var property = JsonSerializer.Deserialize<YoukuCommentProperty>(item.Propertis);
|
||||
if (property != null)
|
||||
{
|
||||
danmakuText.Color = property.Color;
|
||||
}
|
||||
|
||||
danmaku.Items.Add(danmakuText);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return danmaku;
|
||||
}
|
||||
|
||||
|
||||
private string NormalizeSearchName(string name)
|
||||
{
|
||||
// 去掉可能存在的季名称
|
||||
return Regex.Replace(name, @"\s*第.季", "");
|
||||
}
|
||||
}
|
||||
335
Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs
Normal file
335
Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs
Normal file
@@ -0,0 +1,335 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using ComposableAsync;
|
||||
using Jellyfin.Plugin.Danmu.Core.Extensions;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
|
||||
using Jellyfin.Plugin.Danmu.Scrapers.Youku.Entity;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RateLimiter;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Youku;
|
||||
|
||||
public class YoukuApi : AbstractApi
|
||||
{
|
||||
const string HTTP_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/93.0.4577.63";
|
||||
private static readonly object _lock = new object();
|
||||
private static readonly Regex yearReg = new Regex(@"[12][890][0-9][0-9]", RegexOptions.Compiled);
|
||||
private static readonly Regex moviesReg = new Regex(@"<a.*?h5-show-card.*?>([\w\W]+?)</a>", RegexOptions.Compiled);
|
||||
private static readonly Regex trackInfoReg = new Regex(@"data-trackinfo=""(\{[\w\W]+?\})""", RegexOptions.Compiled);
|
||||
private static readonly Regex featureReg = new Regex(@"<div.*?show-feature.*?>([\w\W]+?)</div>", RegexOptions.Compiled);
|
||||
private static readonly Regex unusedReg = new Regex(@"\[.+?\]|\(.+?\)|【.+?】", RegexOptions.Compiled);
|
||||
|
||||
private DateTime lastRequestTime = DateTime.Now.AddDays(-1);
|
||||
|
||||
private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000));
|
||||
|
||||
protected string _cna = string.Empty;
|
||||
protected string _token = string.Empty;
|
||||
protected string _tokenEnc = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="YoukuApi"/> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public YoukuApi(ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory.CreateLogger<YoukuApi>())
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT);
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<YoukuTrackInfo>> SearchAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
return new List<YoukuTrackInfo>();
|
||||
}
|
||||
|
||||
var cacheKey = $"search_{keyword}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
if (_memoryCache.TryGetValue<List<YoukuTrackInfo>>(cacheKey, out var searchResult))
|
||||
{
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
await this.LimitRequestFrequently();
|
||||
|
||||
keyword = HttpUtility.UrlEncode(keyword);
|
||||
var url = $"https://search.youku.com/search_video?keyword={keyword}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var result = new List<YoukuTrackInfo>();
|
||||
var matchs = moviesReg.Matches(body);
|
||||
foreach (Match match in matchs)
|
||||
{
|
||||
var text = HttpUtility.HtmlDecode(match.Groups[1].Value);
|
||||
var trackInfoJson = trackInfoReg.FirstMatchGroup(text);
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(trackInfoJson))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trackInfo = JsonSerializer.Deserialize<YoukuTrackInfo>(trackInfoJson);
|
||||
if (trackInfo != null && trackInfo.ObjectType == 101 && !trackInfo.ObjectTitle.Contains("中配版"))
|
||||
{
|
||||
var featureInfo = featureReg.FirstMatchGroup(text);
|
||||
var year = yearReg.FirstMatch(featureInfo).ToInt();
|
||||
trackInfo.Year = year > 0 ? year : null;
|
||||
trackInfo.Type = featureInfo.Contains("电影") ? "movie" : "tv";
|
||||
trackInfo.ObjectTitle = unusedReg.Replace(trackInfo.ObjectTitle, "");
|
||||
result.Add(trackInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_memoryCache.Set<List<YoukuTrackInfo>>(cacheKey, result, expiredOption);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<YoukuVideo?> GetVideoAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"media_{id}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
if (_memoryCache.TryGetValue<YoukuVideo?>(cacheKey, out var video))
|
||||
{
|
||||
return video;
|
||||
}
|
||||
|
||||
var url = $"https://openapi.youku.com/v2/shows/videos.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&ext=show&show_id={id}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<YoukuVideo>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null)
|
||||
{
|
||||
_memoryCache.Set<YoukuVideo?>(cacheKey, result, expiredOption);
|
||||
return result;
|
||||
}
|
||||
|
||||
_memoryCache.Set<YoukuVideo?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public async Task<YoukuEpisode?> GetEpisodeAsync(string vid, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(vid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var cacheKey = $"episode_{vid}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
|
||||
if (_memoryCache.TryGetValue<YoukuEpisode?>(cacheKey, out var episode))
|
||||
{
|
||||
return episode;
|
||||
}
|
||||
|
||||
// 文档:https://cloud.youku.com/docs?id=46
|
||||
var url = $"https://openapi.youku.com/v2/videos/show_basic.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&video_id={vid}";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<YoukuEpisode>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null)
|
||||
{
|
||||
_memoryCache.Set<YoukuEpisode?>(cacheKey, result, expiredOption);
|
||||
return result;
|
||||
}
|
||||
|
||||
_memoryCache.Set<YoukuEpisode?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<List<YoukuComment>> GetDanmuContentAsync(string vid, CancellationToken cancellationToken)
|
||||
{
|
||||
var danmuList = new List<YoukuComment>();
|
||||
if (string.IsNullOrEmpty(vid))
|
||||
{
|
||||
return danmuList;
|
||||
}
|
||||
|
||||
await EnsureTokenCookie(cancellationToken);
|
||||
|
||||
|
||||
var episode = await this.GetEpisodeAsync(vid, cancellationToken);
|
||||
if (episode == null)
|
||||
{
|
||||
return danmuList;
|
||||
}
|
||||
|
||||
var totalMat = episode.TotalMat;
|
||||
for (int mat = 0; mat < totalMat; mat++)
|
||||
{
|
||||
var comments = await this.GetDanmuContentByMatAsync(vid, mat, cancellationToken);
|
||||
danmuList.AddRange(comments);
|
||||
}
|
||||
|
||||
return danmuList;
|
||||
}
|
||||
|
||||
// mat从0开始,视频分钟数
|
||||
public async Task<List<YoukuComment>> GetDanmuContentByMatAsync(string vid, int mat, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(vid))
|
||||
{
|
||||
return new List<YoukuComment>();
|
||||
}
|
||||
|
||||
await EnsureTokenCookie(cancellationToken);
|
||||
|
||||
|
||||
var ctime = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();
|
||||
var msg = new Dictionary<string, object>() {
|
||||
{"pid", 0},
|
||||
{"ctype", 10004},
|
||||
{"sver", "3.1.0"},
|
||||
{"cver", "v1.0"},
|
||||
{"ctime" , ctime},
|
||||
{"guid", this._cna},
|
||||
{"vid", vid},
|
||||
{"mat", mat},
|
||||
{"mcount", 1},
|
||||
{"type", 1}
|
||||
};
|
||||
|
||||
// 需key按字母排序
|
||||
var msgOrdered = msg.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value).ToJson();
|
||||
var msgEnc = Convert.ToBase64String(Encoding.UTF8.GetBytes(msgOrdered));
|
||||
var sign = this.generateMsgSign(msgEnc);
|
||||
msg.Add("msg", msgEnc);
|
||||
msg.Add("sign", sign);
|
||||
|
||||
|
||||
var appKey = "24679788";
|
||||
var data = msg.ToJson();
|
||||
var t = Convert.ToString(new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds());
|
||||
var param = HttpUtility.ParseQueryString(string.Empty);
|
||||
param["jsv"] = "2.7.0";
|
||||
param["appKey"] = appKey;
|
||||
param["t"] = t;
|
||||
param["sign"] = this.generateTokenSign(t, appKey, data);
|
||||
param["api"] = "mopen.youku.danmu.list";
|
||||
param["v"] = "1.0";
|
||||
param["type"] = "originaljson";
|
||||
param["dataType"] = "jsonp";
|
||||
param["timeout"] = "20000";
|
||||
param["jsonpIncPrefix"] = "utility";
|
||||
|
||||
var builder = new UriBuilder("https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/");
|
||||
builder.Query = param.ToString();
|
||||
HttpResponseMessage response;
|
||||
var formContent = new FormUrlEncodedContent(new[]{
|
||||
new KeyValuePair<string, string>("data", data)
|
||||
});
|
||||
using (var requestMessage = new HttpRequestMessage(HttpMethod.
|
||||
Post, builder.Uri.ToString())
|
||||
{ Content = formContent })
|
||||
{
|
||||
requestMessage.Headers.Add("Referer", "https://v.youku.com");
|
||||
|
||||
response = await httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var dd = await response.Content.ReadAsStringAsync();
|
||||
var result = await response.Content.ReadFromJsonAsync<YoukuRpcResult>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (result != null && !string.IsNullOrEmpty(result.Data.Result))
|
||||
{
|
||||
var commentResult = JsonSerializer.Deserialize<YoukuCommentResult>(result.Data.Result);
|
||||
if (commentResult != null && commentResult.Data != null)
|
||||
{
|
||||
return commentResult.Data.Result;
|
||||
}
|
||||
}
|
||||
|
||||
return new List<YoukuComment>();
|
||||
}
|
||||
|
||||
protected async Task LimitRequestFrequently()
|
||||
{
|
||||
await this._timeConstraint;
|
||||
}
|
||||
|
||||
protected async Task EnsureTokenCookie(CancellationToken cancellationToken)
|
||||
{
|
||||
var cookies = _cookieContainer.GetCookies(new Uri("https://mmstat.com", UriKind.Absolute));
|
||||
var cookie = cookies.FirstOrDefault(x => x.Name == "cna");
|
||||
if (cookie == null)
|
||||
{
|
||||
var url = "https://log.mmstat.com/eg.js";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
// 重新读取最新
|
||||
cookies = _cookieContainer.GetCookies(new Uri("https://mmstat.com", UriKind.Absolute));
|
||||
cookie = cookies.FirstOrDefault(x => x.Name == "cna");
|
||||
}
|
||||
if (cookie != null)
|
||||
{
|
||||
_cna = cookie.Value;
|
||||
}
|
||||
|
||||
|
||||
cookies = _cookieContainer.GetCookies(new Uri("https://youku.com", UriKind.Absolute));
|
||||
var tokenCookie = cookies.FirstOrDefault(x => x.Name == "_m_h5_tk");
|
||||
var tokenEncCookie = cookies.FirstOrDefault(x => x.Name == "_m_h5_tk_enc");
|
||||
if (tokenCookie == null || tokenEncCookie == null)
|
||||
{
|
||||
var url = "https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788";
|
||||
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
// 重新读取最新
|
||||
cookies = _cookieContainer.GetCookies(new Uri("https://youku.com", UriKind.Absolute));
|
||||
tokenCookie = cookies.FirstOrDefault(x => x.Name == "_m_h5_tk");
|
||||
tokenEncCookie = cookies.FirstOrDefault(x => x.Name == "_m_h5_tk_enc");
|
||||
}
|
||||
|
||||
if (tokenCookie != null)
|
||||
{
|
||||
_token = tokenCookie.Value;
|
||||
}
|
||||
if (tokenEncCookie != null)
|
||||
{
|
||||
_tokenEnc = tokenEncCookie.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected string generateMsgSign(string msgEnc)
|
||||
{
|
||||
return (msgEnc + "MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr").ToMD5().ToLower();
|
||||
}
|
||||
|
||||
protected string generateTokenSign(string t, string appKey, string data)
|
||||
{
|
||||
var arr = new string[] { this._token.Substring(0, 32), t, appKey, data };
|
||||
return string.Join('&', arr).ToMD5().ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
4
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/AssemblyInfo.cs
vendored
Normal file
4
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/AssemblyInfo.cs
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ComposableAsync.Core.Test")]
|
||||
|
||||
43
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/Awaitable/DispatcherAwaiter.cs
vendored
Normal file
43
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/Dispatcher/ComposedDispatcher.cs
vendored
Normal file
73
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/Dispatcher/DispatcherAdapter.cs
vendored
Normal file
63
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/Dispatcher/NullDispatcher.cs
vendored
Normal file
74
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/DispatcherExtension.cs
vendored
Normal file
90
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/DispatcherManager/IDispatcherManager.cs
vendored
Normal file
19
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/DispatcherManager/MonoDispatcherManager.cs
vendored
Normal file
36
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/DispatcherProviderExtension.cs
vendored
Normal file
18
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/Disposable/ComposableAsyncDisposable.cs
vendored
Normal file
48
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/ComposableAsync.Core/Disposable/IAsyncDisposable.cs
vendored
Normal file
17
Jellyfin.Plugin.Danmu/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();
|
||||
}
|
||||
}
|
||||
57
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/IBasicDispatcher.cs
vendored
Normal file
57
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/IBasicDispatcher.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
98
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/IDispatcher.cs
vendored
Normal file
98
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/IDispatcher.cs
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
13
Jellyfin.Plugin.Danmu/Vendor/ComposableAsync.Core/IDispatcherProvider.cs
vendored
Normal file
13
Jellyfin.Plugin.Danmu/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.Danmu/Vendor/RateLimiter/AwaitableConstraintExtension.cs
vendored
Normal file
22
Jellyfin.Plugin.Danmu/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/ComposedAwaitableConstraint.cs
vendored
Normal file
47
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/ComposedAwaitableConstraint.cs
vendored
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/CountByIntervalAwaitableConstraint.cs
vendored
Normal file
120
Jellyfin.Plugin.Danmu/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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/DisposeAction.cs
vendored
Normal file
20
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/DisposeAction.cs
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/IAwaitableConstraint.cs
vendored
Normal file
29
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/IAwaitableConstraint.cs
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
26
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/ITime.cs
vendored
Normal file
26
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/ITime.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
35
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/LimitedSizeStack.cs
vendored
Normal file
35
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/LimitedSizeStack.cs
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/PersistentCountByIntervalAwaitableConstraint.cs
vendored
Normal file
42
Jellyfin.Plugin.Danmu/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);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/TimeLimiter.cs
vendored
Normal file
206
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/TimeLimiter.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/TimeSystem.cs
vendored
Normal file
30
Jellyfin.Plugin.Danmu/Vendor/RateLimiter/TimeSystem.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
# jellyfin-plugin-danmu
|
||||
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-danmu/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-danmu/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-danmu/releases)
|
||||
[](https://github.com/cxfksword/jellyfin-plugin-danmu/main/LICENSE)
|
||||
|
||||
jellyfin的弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play。
|
||||
jellyfin的弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play,优酷。
|
||||
|
||||
支持功能:
|
||||
|
||||
@@ -82,7 +82,3 @@ $ dotnet publish Jellyfin.Plugin.Danmu/Jellyfin.Plugin.Danmu.csproj
|
||||
|
||||
[downkyi](https://github.com/leiurayer/downkyi)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Reference in New Issue
Block a user