From ca922ced0437624138e66fde814462c95fc58de8 Mon Sep 17 00:00:00 2001 From: cxfksword <718792+cxfksword@users.noreply.github.com> Date: Sat, 18 Feb 2023 14:29:52 +0800 Subject: [PATCH] Support tencent danmu --- Jellyfin.Plugin.Danmu.Test/BaseTest.cs | 22 ++ Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs | 77 ++++++ Jellyfin.Plugin.Danmu.Test/TencentTest.cs | 147 +++++++++++ Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs | 2 +- .../Core/CanIgnoreException.cs | 21 ++ .../Core/Extensions/ListExtension.cs | 23 ++ .../Core/Extensions/UriExtension.cs | 18 ++ .../Core/Http/HttpClientHandlerEx.cs | 4 +- .../Core/Http/HttpLoggingHandler.cs | 8 +- .../DanmuSubtitleProvider.cs | 3 +- Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs | 42 ++- .../Scrapers/Bilibili/BilibiliApi.cs | 29 +-- .../Scrapers/Entity/ScraperDanmaku.cs | 10 +- .../Scrapers/Iqiyi/IqiyiApi.cs | 10 +- .../Scrapers/Tencent/Entity/TencentComment.cs | 33 +++ .../Tencent/Entity/TencentCommentResult.cs | 23 ++ .../Entity/TencentCommentSegemntResult.cs | 10 + .../Scrapers/Tencent/Entity/TencentEpisode.cs | 17 ++ .../Entity/TencentEpisodeListRequest.cs | 29 +++ .../Entity/TencentEpisodeListResult.cs | 44 ++++ .../Tencent/Entity/TencentSearchDoc.cs | 13 + .../Tencent/Entity/TencentSearchRequest.cs | 31 +++ .../Tencent/Entity/TencentSearchResult.cs | 33 +++ .../Scrapers/Tencent/Entity/TencentVideo.cs | 42 +++ .../Tencent/ExternalId/EpisodeExternalId.cs | 31 +++ .../Tencent/ExternalId/MovieExternalId.cs | 31 +++ .../Tencent/ExternalId/SeasonExternalId.cs | 33 +++ .../Scrapers/Tencent/Tencent.cs | 243 ++++++++++++++++++ .../Scrapers/Tencent/TencentApi.cs | 178 +++++++++++++ .../Scrapers/Youku/YoukuApi.cs | 11 +- 30 files changed, 1173 insertions(+), 45 deletions(-) create mode 100644 Jellyfin.Plugin.Danmu.Test/BaseTest.cs create mode 100644 Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs create mode 100644 Jellyfin.Plugin.Danmu.Test/TencentTest.cs create mode 100644 Jellyfin.Plugin.Danmu/Core/CanIgnoreException.cs create mode 100644 Jellyfin.Plugin.Danmu/Core/Extensions/UriExtension.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentComment.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentResult.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentSegemntResult.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisode.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListRequest.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListResult.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchDoc.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchRequest.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchResult.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentVideo.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/EpisodeExternalId.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/MovieExternalId.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/SeasonExternalId.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs create mode 100644 Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs diff --git a/Jellyfin.Plugin.Danmu.Test/BaseTest.cs b/Jellyfin.Plugin.Danmu.Test/BaseTest.cs new file mode 100644 index 0000000..3dc1ef1 --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/BaseTest.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class BaseTest + { + protected ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + builder.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + })); + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs b/Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs new file mode 100644 index 0000000..fd755a3 --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs @@ -0,0 +1,77 @@ +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.Tencent; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class TencentApiTest : BaseTest + { + [TestMethod] + public void TestSearch() + { + Task.Run(async () => + { + try + { + var keyword = "流浪地球"; + var api = new TencentApi(loggerFactory); + var result = await api.SearchAsync(keyword, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + + [TestMethod] + public void TestGetVideo() + { + Task.Run(async () => + { + try + { + var vid = "mzc00200koowgko"; + var api = new TencentApi(loggerFactory); + var result = await api.GetVideoAsync(vid, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + + [TestMethod] + public void TestGetDanmu() + { + + Task.Run(async () => + { + try + { + var vid = "a00149qxvfz"; + var api = new TencentApi(loggerFactory); + var result = await api.GetDanmuContentAsync(vid, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + } +} diff --git a/Jellyfin.Plugin.Danmu.Test/TencentTest.cs b/Jellyfin.Plugin.Danmu.Test/TencentTest.cs new file mode 100644 index 0000000..7802ace --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/TencentTest.cs @@ -0,0 +1,147 @@ +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; +using Jellyfin.Plugin.Danmu.Scrapers.Tencent; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class TencentTest : BaseTest + { + [TestMethod] + public void TestAddMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Tencent.Tencent(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "流浪地球" + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Add }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Add); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + [TestMethod] + public void TestUpdateMovie() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Tencent.Tencent(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "少年派的奇幻漂流", + ProviderIds = new Dictionary() { { Tencent.ScraperProviderId, "19rrjv4kz0" } }, + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Update }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + + + + [TestMethod] + public void TestAddSeason() + { + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Tencent.Tencent(loggerFactory)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Season + { + Name = "三体", + ProductionYear = 2023, + }; + + var list = new List(); + list.Add(new LibraryEvent { Item = item, EventType = EventType.Add }); + + Task.Run(async () => + { + try + { + await libraryManagerEventsHelper.ProcessQueuedSeasonEvents(list, EventType.Add); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + [TestMethod] + public void TestGetMedia() + { + + Task.Run(async () => + { + try + { + var api = new Tencent(loggerFactory); + var media = await api.GetMedia(new Season(), "mzc002006n62s11"); + Console.WriteLine(media); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + } +} diff --git a/Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs b/Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs index 963fd10..2b51229 100644 --- a/Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs +++ b/Jellyfin.Plugin.Danmu.Test/YoukuApiTest.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Plugin.Danmu.Test [TestMethod] public void TestSearch() { - var keyword = "一拳超人"; + var keyword = "西虹市首富"; var api = new YoukuApi(loggerFactory); Task.Run(async () => diff --git a/Jellyfin.Plugin.Danmu/Core/CanIgnoreException.cs b/Jellyfin.Plugin.Danmu/Core/CanIgnoreException.cs new file mode 100644 index 0000000..5d83bf3 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Core/CanIgnoreException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Jellyfin.Plugin.Danmu.Core; + +class CanIgnoreException : Exception +{ + public CanIgnoreException(string message) : base(message) + { + } + + /// + /// Don't display call stack as it's irrelevant + /// + public override string StackTrace + { + get + { + return ""; + } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Core/Extensions/ListExtension.cs b/Jellyfin.Plugin.Danmu/Core/Extensions/ListExtension.cs index d31cb40..cac4434 100644 --- a/Jellyfin.Plugin.Danmu/Core/Extensions/ListExtension.cs +++ b/Jellyfin.Plugin.Danmu/Core/Extensions/ListExtension.cs @@ -11,5 +11,28 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable self) => self.Select((item, index) => (item, index)); + /// + /// 从list抽取间隔指定大小数量的item + /// + public static IEnumerable ExtractToNumber(this IEnumerable self, int limit) + { + + var count = self.Count(); + var step = (int)Math.Ceiling((double)count / limit); + var list = new List(); + var idx = 0; + for (var i = 0; i < limit; i++) + { + if (idx >= count) + { + break; + } + list.Add(self.ElementAt(idx)); + idx += step; + } + + return list; + } + } } diff --git a/Jellyfin.Plugin.Danmu/Core/Extensions/UriExtension.cs b/Jellyfin.Plugin.Danmu/Core/Extensions/UriExtension.cs new file mode 100644 index 0000000..8162ef2 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Core/Extensions/UriExtension.cs @@ -0,0 +1,18 @@ +using System; + +namespace Jellyfin.Plugin.Danmu.Core.Extensions +{ + public static class UriExtension + { + public static string GetSecondLevelHost(this Uri uri) + { + var domain = uri.Host; + var arrHost = uri.Host.Split('.'); + if (arrHost.Length >= 2) + { + domain = arrHost[arrHost.Length - 2] + "." + arrHost[arrHost.Length - 1]; + } + return domain; + } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Core/Http/HttpClientHandlerEx.cs b/Jellyfin.Plugin.Danmu/Core/Http/HttpClientHandlerEx.cs index 9c06fd3..238adbc 100644 --- a/Jellyfin.Plugin.Danmu/Core/Http/HttpClientHandlerEx.cs +++ b/Jellyfin.Plugin.Danmu/Core/Http/HttpClientHandlerEx.cs @@ -13,10 +13,10 @@ namespace Jellyfin.Plugin.Danmu.Core.Http { public HttpClientHandlerEx() { - // 忽略SSL证书问题 - ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true; + ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true; // 忽略SSL证书问题 AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; CookieContainer = new CookieContainer(); + UseCookies = true; // 使用cookie } protected override Task SendAsync( diff --git a/Jellyfin.Plugin.Danmu/Core/Http/HttpLoggingHandler.cs b/Jellyfin.Plugin.Danmu/Core/Http/HttpLoggingHandler.cs index 94a1ba3..f5f1efc 100644 --- a/Jellyfin.Plugin.Danmu/Core/Http/HttpLoggingHandler.cs +++ b/Jellyfin.Plugin.Danmu/Core/Http/HttpLoggingHandler.cs @@ -60,10 +60,10 @@ namespace Jellyfin.Plugin.Danmu.Core.Http log.Append("Response: ").Append(response).Append('\n'); } - if (response?.Content != null) - { - log.Append("Content: ").Append(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)).Append('\n'); - } + // if (response?.Content != null) + // { + // log.Append("Content: ").Append(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)).Append('\n'); + // } } catch { diff --git a/Jellyfin.Plugin.Danmu/DanmuSubtitleProvider.cs b/Jellyfin.Plugin.Danmu/DanmuSubtitleProvider.cs index 39c36d7..9a9ddfa 100644 --- a/Jellyfin.Plugin.Danmu/DanmuSubtitleProvider.cs +++ b/Jellyfin.Plugin.Danmu/DanmuSubtitleProvider.cs @@ -19,6 +19,7 @@ using System.Text.Json; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Dto; +using Jellyfin.Plugin.Danmu.Core; namespace Jellyfin.Plugin.Danmu; @@ -74,7 +75,7 @@ public class DanmuSubtitleProvider : ISubtitleProvider _libraryManagerEventsHelper.QueueItem(item, EventType.Force); } - throw new Exception($"弹幕下载已由{Plugin.Instance?.Name}插件接管,请忽略本异常."); + throw new CanIgnoreException($"弹幕下载已由{Plugin.Instance?.Name}插件接管."); } public async Task> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) diff --git a/Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs index dd3438c..ad7a7a5 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/AbstractApi.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using Jellyfin.Extensions.Json; +using Jellyfin.Plugin.Danmu.Core.Extensions; using Jellyfin.Plugin.Danmu.Core.Http; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -12,7 +13,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers; public abstract class AbstractApi : IDisposable { - const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44"; + public const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44"; protected ILogger _logger; protected JsonSerializerOptions _jsonOptions = JsonDefaults.Options; protected HttpClient httpClient; @@ -31,6 +32,45 @@ public abstract class AbstractApi : IDisposable _memoryCache = new MemoryCache(new MemoryCacheOptions()); } + protected void AddCookies(string cookieVal, Uri uri) + { + // 清空旧的cookie + var cookies = _cookieContainer.GetCookies(uri); + foreach (Cookie co in cookies) + { + co.Expires = DateTime.Now.Subtract(TimeSpan.FromDays(1)); + } + + + // 附加新的cookie + if (!string.IsNullOrEmpty(cookieVal)) + { + var domain = uri.GetSecondLevelHost(); + var arr = cookieVal.Split(';'); + foreach (var str in arr) + { + var cookieArr = str.Split('='); + if (cookieArr.Length != 2) + { + continue; + } + + var key = cookieArr[0].Trim(); + var value = cookieArr[1].Trim(); + try + { + _cookieContainer.Add(new Cookie(key, value, "/", "." + domain)); + } + catch (Exception ex) + { + this._logger.LogError(ex, ex.Message); + } + } + + } + + } + public void Dispose() diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs index ed379a9..080cc78 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Bilibili/BilibiliApi.cs @@ -26,14 +26,8 @@ using ComposableAsync; namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili; -public class BilibiliApi : IDisposable +public class BilibiliApi : AbstractApi { - const string HTTP_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44"; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private HttpClient httpClient; - private CookieContainer _cookieContainer; - private readonly IMemoryCache _memoryCache; private static readonly object _lock = new object(); private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); @@ -42,14 +36,8 @@ public class BilibiliApi : IDisposable /// /// The . public BilibiliApi(ILoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) { - _logger = loggerFactory.CreateLogger(); - - var handler = new HttpClientHandlerEx(); - _cookieContainer = handler.CookieContainer; - httpClient = new HttpClient(handler, true); - httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT); - _memoryCache = new MemoryCache(new MemoryCacheOptions()); } @@ -284,18 +272,5 @@ public class BilibiliApi : IDisposable await this._timeConstraint; } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _memoryCache.Dispose(); - } - } } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperDanmaku.cs b/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperDanmaku.cs index d753212..d23483c 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperDanmaku.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Entity/ScraperDanmaku.cs @@ -85,8 +85,14 @@ public class ScraperDanmaku public class ScraperDanmakuText : IXmlSerializable { public long Id { get; set; } //弹幕dmID - public int Progress { get; set; } //出现时间(单位ms) - public int Mode { get; set; } //弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2) + /// + /// 出现时间(单位ms) + /// + public int Progress { get; set; } + /// + /// 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2) + /// + public int Mode { get; set; } public int Fontsize { get; set; } = 25; //文字大小 public uint Color { get; set; } //弹幕颜色 public string MidHash { get; set; } //发送者UID的HASH diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs index 3f7abca..7940704 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs @@ -25,7 +25,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi; public class IqiyiApi : 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(@"([\w\W]+?)", RegexOptions.Compiled); @@ -38,6 +37,7 @@ public class IqiyiApi : AbstractApi private DateTime lastRequestTime = DateTime.Now.AddDays(-1); private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); + private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100)); protected string _cna = string.Empty; protected string _token = string.Empty; @@ -50,7 +50,6 @@ public class IqiyiApi : AbstractApi public IqiyiApi(ILoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { - httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT); } @@ -258,7 +257,9 @@ public class IqiyiApi : AbstractApi try { var comments = await this.GetDanmuContentByMatAsync(tvId, mat, cancellationToken); - danmuList.AddRange(comments); + + // 每段有300秒弹幕,为避免弹幕太大,从中间隔抽取最大60秒200条弹幕 + danmuList.AddRange(comments.ExtractToNumber(1000)); } catch (Exception ex) { @@ -266,6 +267,9 @@ public class IqiyiApi : AbstractApi } mat++; + + // 等待一段时间避免api请求太快 + await _delayExecuteConstraint; } while (mat < 1000); return danmuList; diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentComment.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentComment.cs new file mode 100644 index 0000000..8834549 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentComment.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentComment +{ + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("content")] + public string Content { get; set; } + [JsonPropertyName("content_score")] + public double ContentScore { get; set; } + [JsonPropertyName("content_style")] + public string ContentStyle { get; set; } + [JsonPropertyName("create_time")] + public string CreateTime { get; set; } + [JsonPropertyName("show_weight")] + public int ShowWeight { get; set; } + [JsonPropertyName("time_offset")] + public string TimeOffset { get; set; } + [JsonPropertyName("nick")] + public string Nick { get; set; } + +} + +public class TencentCommentContentStyle +{ + [JsonPropertyName("color")] + public string Color { get; set; } + [JsonPropertyName("position")] + public int Position { get; set; } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentResult.cs new file mode 100644 index 0000000..5dfea07 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentCommentResult +{ + [JsonPropertyName("segment_span")] + public string SegmentSpan { get; set; } + [JsonPropertyName("segment_start")] + public string SegmentStart { get; set; } + + [JsonPropertyName("segment_index")] + public Dictionary SegmentIndex { get; set; } +} + +public class TencentCommentSegment +{ + [JsonPropertyName("segment_name")] + public string SegmentName { get; set; } + [JsonPropertyName("segment_start")] + public string SegmentStart { get; set; } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentSegemntResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentSegemntResult.cs new file mode 100644 index 0000000..9c4b6f0 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentCommentSegemntResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentCommentSegmentResult +{ + [JsonPropertyName("barrage_list")] + public List BarrageList { get; set; } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisode.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisode.cs new file mode 100644 index 0000000..e088826 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisode.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentEpisode +{ + [JsonPropertyName("vid")] + public string Vid { get; set; } + [JsonPropertyName("cid")] + public string Cid { get; set; } + [JsonPropertyName("duration")] + public string Duration { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } + [JsonPropertyName("is_trailer")] + public string IsTrailer { get; set; } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListRequest.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListRequest.cs new file mode 100644 index 0000000..35e08f1 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentEpisodeListRequest +{ + [JsonPropertyName("page_params")] + public TencentPageParams PageParams { get; set; } +} + +public class TencentPageParams +{ + [JsonPropertyName("page_type")] + public string PageType { get; set; } = "detail_operation"; + [JsonPropertyName("page_id")] + public string PageId { get; set; } = "vsite_episode_list"; + [JsonPropertyName("id_type")] + public string IdType { get; set; } = "1"; + [JsonPropertyName("page_size")] + public string PageSize { get; set; } = "100"; + [JsonPropertyName("cid")] + public string Cid { get; set; } + [JsonPropertyName("lid")] + public string Lid { get; set; } = "0"; + [JsonPropertyName("req_from")] + public string ReqFrom { get; set; } = "web_mobile"; + [JsonPropertyName("page_context")] + public string PageContext { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListResult.cs new file mode 100644 index 0000000..8f2b23a --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentEpisodeListResult.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentEpisodeListResult +{ + [JsonPropertyName("data")] + public TencentModuleDataList Data { get; set; } +} + +public class TencentModuleDataList +{ + [JsonPropertyName("module_list_datas")] + public List ModuleListDatas { get; set; } +} + +public class TencentModuleList +{ + [JsonPropertyName("module_datas")] + public List ModuleDatas { get; set; } +} + +public class TencentModule +{ + [JsonPropertyName("item_data_lists")] + public TencentModuleItemList ItemDataLists { get; set; } +} + +public class TencentModuleItemList +{ + [JsonPropertyName("item_datas")] + public List ItemDatas { get; set; } +} + +public class TencentModuleItem +{ + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + [JsonPropertyName("item_type")] + public string ItemType { get; set; } + [JsonPropertyName("item_params")] + public TencentEpisode ItemParams { get; set; } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchDoc.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchDoc.cs new file mode 100644 index 0000000..ef7c5ae --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchDoc.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentSearchDoc +{ + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("dataType")] + public int DataType { get; set; } +} + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchRequest.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchRequest.cs new file mode 100644 index 0000000..d9781b9 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchRequest.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentSearchRequest +{ + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + [JsonPropertyName("filterValue")] + public string FilterValue { get; set; } = "firstTabid=150"; + [JsonPropertyName("retry")] + public int Retry { get; set; } = 0; + [JsonPropertyName("query")] + public string Query { get; set; } + [JsonPropertyName("pagenum")] + public int PageNum { get; set; } = 0; + [JsonPropertyName("pagesize")] + public int PageSize { get; set; } = 20; + [JsonPropertyName("queryFrom")] + public int QueryFrom { get; set; } = 4; + [JsonPropertyName("isneedQc")] + public bool IsneedQc { get; set; } = true; + [JsonPropertyName("adRequestInfo")] + public string AdRequestInfo { get; set; } = string.Empty; + [JsonPropertyName("sdkRequestInfo")] + public string SdkRequestInfo { get; set; } = string.Empty; + [JsonPropertyName("sceneId")] + public int SceneId { get; set; } = 21; + [JsonPropertyName("platform")] + public string Platform { get; set; } = "23"; +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchResult.cs new file mode 100644 index 0000000..60ca852 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentSearchResult.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentSearchResult +{ + [JsonPropertyName("data")] + public TencentSearchData Data { get; set; } +} + +public class TencentSearchData +{ + [JsonPropertyName("normalList")] + public TencentSearchBox NormalList { get; set; } +} + +public class TencentSearchBox +{ + [JsonPropertyName("itemList")] + public List ItemList { get; set; } +} + +public class TencentSearchItem +{ + [JsonPropertyName("doc")] + public TencentSearchDoc Doc { get; set; } + [JsonPropertyName("videoInfo")] + public TencentVideo VideoInfo { get; set; } + +} + + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentVideo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentVideo.cs new file mode 100644 index 0000000..05c87e9 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Entity/TencentVideo.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.Entity; + +public class TencentVideo +{ + private static readonly Regex regHtml = new Regex(@"<.+?>", RegexOptions.Compiled); + + [JsonIgnore] + public string Id { get; set; } + [JsonPropertyName("videoType")] + public int VideoType { get; set; } + [JsonPropertyName("typeName")] + public string TypeName { get; set; } + private string _title = string.Empty; + [JsonPropertyName("title")] + public string Title + { + get + { + return regHtml.Replace(_title, ""); + } + set + { + _title = value; + } + } + [JsonPropertyName("year")] + public int? Year { get; set; } + [JsonPropertyName("subjectDoc")] + public TencentSubjectDoc SubjectDoc { get; set; } + [JsonIgnore] + public List EpisodeList { get; set; } +} + +public class TencentSubjectDoc +{ + [JsonPropertyName("videoNum")] + public int VideoNum { get; set; } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/EpisodeExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/EpisodeExternalId.cs new file mode 100644 index 0000000..da45ee9 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/EpisodeExternalId.cs @@ -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.Tencent.ExternalId +{ + /// + public class EpisodeExternalId : IExternalId + { + /// + public string ProviderName => Tencent.ScraperProviderName; + + /// + public string Key => Tencent.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Episode; + + /// + public string UrlFormatString => "#"; + + /// + public bool Supports(IHasProviderIds item) => item is Episode; + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/MovieExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/MovieExternalId.cs new file mode 100644 index 0000000..3b7920c --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/MovieExternalId.cs @@ -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.Tencent.ExternalId +{ + /// + public class MovieExternalId : IExternalId + { + /// + public string ProviderName => Tencent.ScraperProviderName; + + /// + public string Key => Tencent.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => null; + + /// + public string UrlFormatString => "https://v.qq.com/x/cover/{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Movie; + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/SeasonExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/SeasonExternalId.cs new file mode 100644 index 0000000..d6e0620 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/ExternalId/SeasonExternalId.cs @@ -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.Tencent.ExternalId +{ + + /// + public class SeasonExternalId : IExternalId + { + /// + public string ProviderName => Tencent.ScraperProviderName; + + /// + public string Key => Tencent.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => null; + + /// + public string UrlFormatString => "https://v.qq.com/x/cover/{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Season; + } + +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs new file mode 100644 index 0000000..c1947da --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs @@ -0,0 +1,243 @@ +using System.Web; +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.Tencent.Entity; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent; + +public class Tencent : AbstractScraper +{ + public const string ScraperProviderName = "腾讯"; + public const string ScraperProviderId = "TencentID"; + + private readonly TencentApi _api; + + public Tencent(ILoggerFactory logManager) + : base(logManager.CreateLogger()) + { + _api = new TencentApi(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> Search(BaseItem item) + { + var list = new List(); + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + 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.Id; + var title = video.Title; + var pubYear = video.Year; + + if (isMovieItemType && video.TypeName != "电影") + { + continue; + } + + if (!isMovieItemType && video.TypeName == "电影") + { + continue; + } + + // 检测标题是否相似(越大越相似) + var score = searchName.Distance(title); + if (score < 0.7) + { + continue; + } + + list.Add(new ScraperSearchInfo() + { + Id = $"{videoId}", + Name = title, + Category = video.TypeName, + Year = pubYear, + }); + } + + + return list; + } + + public override async Task SearchMediaId(BaseItem item) + { + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + 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.Id; + var title = video.Title; + var pubYear = video.Year; + + if (isMovieItemType && video.TypeName != "电影") + { + continue; + } + + if (!isMovieItemType && video.TypeName == "电影") + { + 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}] 发行年份不一致,忽略处理. year: {1} jellyfin: {2}", title, pubYear, itemPubYear); + continue; + } + + return video.Id; + } + + return null; + } + + + public override async Task GetMedia(BaseItem item, string id) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + 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; + if (isMovieItemType && video.EpisodeList != null && video.EpisodeList.Count > 0) + { + media.CommentId = $"{video.EpisodeList[0].Vid}"; + } + if (video.EpisodeList != null && video.EpisodeList.Count > 0) + { + foreach (var ep in video.EpisodeList) + { + media.Episodes.Add(new ScraperEpisode() { Id = $"{ep.Vid}", CommentId = $"{ep.Vid}" }); + } + } + + return media; + } + + public override async Task GetMediaEpisode(BaseItem item, string id) + { + + var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; + if (isMovieItemType) + { + var video = await _api.GetVideoAsync(id, CancellationToken.None).ConfigureAwait(false); + if (video == null || video.EpisodeList == null || video.EpisodeList.Count <= 0) + { + return null; + } + + return new ScraperEpisode() { Id = id, CommentId = $"{video.EpisodeList[0].Vid}" }; + } + + return new ScraperEpisode() { Id = id, CommentId = id }; + } + + public override async Task GetDanmuContent(BaseItem item, 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 = "dm.video.qq.com"; + foreach (var comment in comments) + { + try + { + var midHash = string.IsNullOrEmpty(comment.Nick) ? "anonymous".ToBase64() : comment.Nick.ToBase64(); + var mode = 1; + var danmakuText = new ScraperDanmakuText(); + danmakuText.Progress = comment.TimeOffset.ToInt(); + danmakuText.Mode = 1; + danmakuText.MidHash = $"[tencent]{midHash}"; + danmakuText.Id = comment.Id.ToLong(); + danmakuText.Content = comment.Content.Replace("VIP :", ""); + if (!string.IsNullOrEmpty(comment.ContentStyle)) + { + var style = comment.ContentStyle.FromJson(); + if (style != null && uint.TryParse(style.Color, System.Globalization.NumberStyles.HexNumber, null, out var color)) + { + danmakuText.Color = color; + } + + if (style != null && style.Position > 0) + { + switch (style.Position) + { + case 2:// top + danmakuText.Mode = 5; + break; + case 3:// bottom + danmakuText.Mode = 4; + break; + } + } + } + + danmaku.Items.Add(danmakuText); + } + catch (Exception ex) + { + + } + + } + + return danmaku; + } + + + private string NormalizeSearchName(string name) + { + // 去掉可能存在的季名称 + return Regex.Replace(name, @"\s*第.季", ""); + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs new file mode 100644 index 0000000..397ed64 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs @@ -0,0 +1,178 @@ +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.Tencent.Entity; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using RateLimiter; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent; + +public class TencentApi : AbstractApi +{ + 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(@"([\w\W]+?)", RegexOptions.Compiled); + private static readonly Regex trackInfoReg = new Regex(@"data-trackinfo=""(\{[\w\W]+?\})""", RegexOptions.Compiled); + private static readonly Regex featureReg = new Regex(@"([\w\W]+?)", 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)); + private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100)); + + + protected string _cna = string.Empty; + protected string _token = string.Empty; + protected string _tokenEnc = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public TencentApi(ILoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) + { + httpClient.DefaultRequestHeaders.Add("referer", "https://v.qq.com/"); + this.AddCookies("pgv_pvid=40b67e3b06027f3d; video_platform=2; vversion_name=8.2.95; video_bucketid=4; video_omgid=0a1ff6bc9407c0b1cff86ee5d359614d", new Uri("https://v.qq.com")); + } + + + public async Task> SearchAsync(string keyword, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(keyword)) + { + return new List(); + } + + var cacheKey = $"search_{keyword}"; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; + if (_memoryCache.TryGetValue>(cacheKey, out var cacheValue)) + { + return cacheValue; + } + + await this.LimitRequestFrequently(); + + var postData = new TencentSearchRequest() { Query = keyword }; + var url = $"https://pbaccess.video.qq.com/trpc.videosearch.mobile_search.HttpMobileRecall/MbSearchHttp"; + var response = await httpClient.PostAsJsonAsync(url, postData, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = new List(); + var searchResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (searchResult != null && searchResult.Data != null && searchResult.Data.NormalList != null && searchResult.Data.NormalList.ItemList != null) + { + foreach (var item in searchResult.Data.NormalList.ItemList) + { + if (item.VideoInfo.Year == null || item.VideoInfo.Year == 0) + { + continue; + } + + var video = item.VideoInfo; + video.Id = item.Doc.Id; + result.Add(video); + } + } + + _memoryCache.Set>(cacheKey, result, expiredOption); + return result; + } + + public async Task 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(cacheKey, out var video)) + { + return video; + } + + var postData = new TencentEpisodeListRequest() { PageParams = new TencentPageParams() { Cid = id } }; + var url = $"https://pbaccess.video.qq.com/trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData?video_appid=3000010&vplatform=2"; + var response = await httpClient.PostAsJsonAsync(url, postData, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (result != null && result.Data != null && result.Data.ModuleListDatas != null) + { + var videoInfo = new TencentVideo(); + videoInfo.Id = id; + videoInfo.EpisodeList = result.Data.ModuleListDatas.First().ModuleDatas.First().ItemDataLists.ItemDatas.Select(x => x.ItemParams).Where(x => x.IsTrailer != "1").ToList(); + _memoryCache.Set(cacheKey, videoInfo, expiredOption); + return videoInfo; + } + + _memoryCache.Set(cacheKey, null, expiredOption); + return null; + } + + + + + public async Task> GetDanmuContentAsync(string vid, CancellationToken cancellationToken) + { + var danmuList = new List(); + if (string.IsNullOrEmpty(vid)) + { + return danmuList; + } + + + var url = $"https://dm.video.qq.com/barrage/base/{vid}"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (result != null && result.SegmentIndex != null) + { + var start = result.SegmentStart.ToLong(); + var size = result.SegmentSpan.ToLong(); + for (long i = start; result.SegmentIndex.ContainsKey(i) && size > 0; i += size) + { + + var segment = result.SegmentIndex[i]; + var segmentUrl = $"https://dm.video.qq.com/barrage/segment/{vid}/{segment.SegmentName}"; + var segmentResponse = await httpClient.GetAsync(segmentUrl, cancellationToken).ConfigureAwait(false); + segmentResponse.EnsureSuccessStatusCode(); + + var segmentResult = await segmentResponse.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (segmentResult != null && segmentResult.BarrageList != null) + { + // 30秒每segment,为避免弹幕太大,从中间隔抽取最大60秒200条弹幕 + danmuList.AddRange(segmentResult.BarrageList.ExtractToNumber(100)); + } + + // 等待一段时间避免api请求太快 + await _delayExecuteConstraint; + } + } + + return danmuList; + } + + protected async Task LimitRequestFrequently() + { + await this._timeConstraint; + } + +} + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs index 8692d60..c2ccc7b 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs @@ -21,7 +21,6 @@ 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(@"([\w\W]+?)", RegexOptions.Compiled); @@ -32,6 +31,7 @@ public class YoukuApi : AbstractApi private DateTime lastRequestTime = DateTime.Now.AddDays(-1); private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); + private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100)); protected string _cna = string.Empty; protected string _token = string.Empty; @@ -44,7 +44,6 @@ public class YoukuApi : AbstractApi public YoukuApi(ILoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { - httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT); } @@ -65,7 +64,7 @@ public class YoukuApi : AbstractApi await this.LimitRequestFrequently(); keyword = HttpUtility.UrlEncode(keyword); - var ua = HttpUtility.UrlEncode(HTTP_USER_AGENT); + var ua = HttpUtility.UrlEncode(AbstractApi.HTTP_USER_AGENT); var url = $"https://search.youku.com/api/search?keyword={keyword}&userAgent={ua}&site=1&categories=0&ftype=0&ob=0&pg=1"; var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); @@ -189,6 +188,9 @@ public class YoukuApi : AbstractApi { var comments = await this.GetDanmuContentByMatAsync(vid, mat, cancellationToken); danmuList.AddRange(comments); + + // 等待一段时间避免api请求太快 + await _delayExecuteConstraint; } return danmuList; @@ -264,7 +266,8 @@ public class YoukuApi : AbstractApi var commentResult = JsonSerializer.Deserialize(result.Data.Result); if (commentResult != null && commentResult.Data != null) { - return commentResult.Data.Result; + // 每段有60秒弹幕,为避免弹幕太大,从中间隔抽取最大60秒200条弹幕 + return commentResult.Data.Result.ExtractToNumber(200).ToList(); } }