diff --git a/Jellyfin.Plugin.Danmu.Test/MgtvApiTest.cs b/Jellyfin.Plugin.Danmu.Test/MgtvApiTest.cs new file mode 100644 index 0000000..2dcb300 --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/MgtvApiTest.cs @@ -0,0 +1,78 @@ +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.Mgtv; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Danmu.Test +{ + + [TestClass] + public class MgtvApiTest : BaseTest + { + [TestMethod] + public void TestSearch() + { + Task.Run(async () => + { + try + { + var keyword = "大侦探"; + var api = new MgtvApi(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 id = "310102"; + var api = new MgtvApi(loggerFactory); + var result = await api.GetVideoAsync(id, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + + [TestMethod] + public void TestGetDanmu() + { + + Task.Run(async () => + { + try + { + var cid = "514446"; + var vid = "18053294"; + var api = new MgtvApi(loggerFactory); + var result = await api.GetDanmuContentAsync(cid, vid, CancellationToken.None); + Console.WriteLine(result); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + } + + } +} diff --git a/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs b/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs new file mode 100644 index 0000000..54cdd58 --- /dev/null +++ b/Jellyfin.Plugin.Danmu.Test/MgtvTest.cs @@ -0,0 +1,150 @@ +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.Mgtv; +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 MgtvTest : BaseTest + { + [TestMethod] + public void TestAddMovie() + { + var libraryManagerStub = new Mock(); + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = 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 libraryManagerStub = new Mock(); + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = new Mock(); + var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager); + + var item = new Movie + { + Name = "虚颜", + ProviderIds = new Dictionary() { { Mgtv.ScraperProviderId, "519236" } }, + }; + + 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 libraryManagerStub = new Mock(); + var scraperManager = new ScraperManager(loggerFactory); + scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object)); + + var fileSystemStub = new Mock(); + var directoryServiceStub = 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 libraryManagerStub = new Mock(); + var api = new Mgtv(loggerFactory, libraryManagerStub.Object); + var media = await api.GetMedia(new Season(), "514446"); + Console.WriteLine(media); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + }).GetAwaiter().GetResult(); + + } + + } +} diff --git a/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs b/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs index 0ac1fec..8c2c78d 100644 --- a/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs +++ b/Jellyfin.Plugin.Danmu/Core/Extensions/StringExtension.cs @@ -30,15 +30,7 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions return 0; } - public static Int64 ToInt64(this string s) - { - if (Int64.TryParse(s, out var val)) - { - return val; - } - return 0; - } public static float ToFloat(this string s) { diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs index 7940704..3ad4fc7 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs @@ -25,17 +25,9 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi; public class IqiyiApi : 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 static readonly Regex regTvId = new Regex(@"""tvid"":(\d+?),", 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)); diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvComment.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvComment.cs new file mode 100644 index 0000000..9b708cc --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvComment.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvComment +{ + [JsonPropertyName("id")] + public long Id { get; set; } + [JsonPropertyName("ids")] + public string Ids { get; set; } + [JsonPropertyName("type")] + public int Type { get; set; } + [JsonPropertyName("uid")] + public long Uid { get; set; } + [JsonPropertyName("uuid")] + public string Uuid { get; set; } + [JsonPropertyName("content")] + public string Content { get; set; } + [JsonPropertyName("time")] + public int Time { get; set; } + [JsonPropertyName("v2_color")] + public MgtvCommentColor Color { get; set; } + +} + +public class MgtvCommentColor +{ + [JsonPropertyName("color_left")] + public MgtvCommentColorRGB ColorLeft { get; set; } + [JsonPropertyName("color_right")] + public MgtvCommentColorRGB ColorRight { get; set; } +} + + +public class MgtvCommentColorRGB +{ + [JsonPropertyName("r")] + public int R { get; set; } + [JsonPropertyName("g")] + public int G { get; set; } + [JsonPropertyName("b")] + public int B { get; set; } + + public uint HexNumber + { + get + { + return (uint)((R << 16) | (G << 8) | (B)); + } + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvCommentResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvCommentResult.cs new file mode 100644 index 0000000..0e4bce4 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvCommentResult.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvCommentResult +{ + [JsonPropertyName("data")] + public MgtvCommentData Data { get; set; } +} + +public class MgtvCommentData +{ + [JsonPropertyName("cdn_list")] + public string CdnList { get; set; } + [JsonPropertyName("cdn_version")] + public string CdnVersion { get; set; } + + public string CdnHost + { + get + { + if (string.IsNullOrEmpty(CdnList)) + { + return string.Empty; + } + + return CdnList.Split(",").First(); + } + } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvCommentSegemntResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvCommentSegemntResult.cs new file mode 100644 index 0000000..1ed62a3 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvCommentSegemntResult.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvCommentSegmentResult +{ + [JsonPropertyName("data")] + public MgtvCommentSegmentData Data { get; set; } +} + + +public class MgtvCommentSegmentData +{ + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("items")] + public List Items { get; set; } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisode.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisode.cs new file mode 100644 index 0000000..0c18a24 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisode.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvEpisode +{ + [JsonPropertyName("src_clip_id")] + public string SourceClipId { get; set; } + [JsonPropertyName("clip_id")] + public string ClipId { get; set; } + [JsonPropertyName("t1")] + public string Title { get; set; } + [JsonPropertyName("time")] + public string Time { get; set; } + [JsonPropertyName("video_id")] + public string VideoId { get; set; } + [JsonPropertyName("contentType")] + public string ContentType { get; set; } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisodeListRequest.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisodeListRequest.cs new file mode 100644 index 0000000..812e288 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisodeListRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvEpisodeListRequest +{ + [JsonPropertyName("page_params")] + public MgtvPageParams PageParams { get; set; } +} + +public class MgtvPageParams +{ + [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/Mgtv/Entity/MgtvEpisodeListResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisodeListResult.cs new file mode 100644 index 0000000..c330833 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvEpisodeListResult.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvEpisodeListResult +{ + [JsonPropertyName("data")] + public MgtvEpisodeListData Data { get; set; } + +} + + +public class MgtvEpisodeListData +{ + [JsonPropertyName("total")] + public int Total { get; set; } + [JsonPropertyName("tab_m")] + public List Tabs { get; set; } + [JsonPropertyName("pageNo")] + public int PageNo { get; set; } + [JsonPropertyName("pageSize")] + public int PageSize { get; set; } + + [JsonPropertyName("list")] + public List List { get; set; } +} + +public class MgtvEpisodeListTab +{ + [JsonPropertyName("m")] + public string Month { get; set; } +} + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvSearchResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvSearchResult.cs new file mode 100644 index 0000000..22995a1 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvSearchResult.cs @@ -0,0 +1,118 @@ +using System.Linq; +using System.ComponentModel; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Jellyfin.Plugin.Danmu.Core.Extensions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvSearchResult +{ + [JsonPropertyName("data")] + public MgtvSearchData Data { get; set; } +} + +public class MgtvSearchData +{ + [JsonPropertyName("contents")] + public List Contents { get; set; } +} + +public class MgtvSearchContent +{ + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("data")] + public List Data { get; set; } + +} + +public class MgtvSearchItem +{ + private static readonly Regex regHtml = new Regex(@"<.+?>", RegexOptions.Compiled); + private static readonly Regex regId = new Regex(@"\/b\/(\d+)\/(\d+)", RegexOptions.Compiled); + + private static readonly Regex regYear = new Regex(@"[12][890][0-9][0-9]", RegexOptions.Compiled); + + [JsonPropertyName("jumpKind")] + public string JumpKind { get; set; } + [JsonPropertyName("desc")] + public List Desc { get; set; } + + [JsonPropertyName("source")] + public string Source { get; set; } + + private string _title = string.Empty; + [JsonPropertyName("title")] + public string Title + { + get + { + return regHtml.Replace(_title, ""); + } + set + { + _title = value; + } + } + + [JsonPropertyName("url")] + public string Url { get; set; } + + + public string Id + { + get + { + if (string.IsNullOrEmpty(Url)) + { + return string.Empty; + } + + var match = regId.Match(Url); + if (match.Success) + { + return match.Groups[1].Value; + } + + return string.Empty; + } + } + + public string TypeName + { + get + { + if (Desc == null || Desc.Count <= 0) + { + return string.Empty; + } + + return Desc.First().Split("/").First().Replace("类型:", "").Trim(); + } + } + + public int? Year + { + get + { + if (Desc == null || Desc.Count <= 0) + { + return null; + } + + var match = regYear.Match(Desc.First()); + if (match.Success) + { + return match.Value.ToInt(); + } + + return null; + } + } + + +} + + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvVideo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvVideo.cs new file mode 100644 index 0000000..7bc5c8a --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvVideo.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Jellyfin.Plugin.Danmu.Core.Extensions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvVideo +{ + private static readonly Regex regHtml = new Regex(@"<.+?>", RegexOptions.Compiled); + + [JsonPropertyName("videoId")] + 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("time")] + public string Time { get; set; } + [JsonPropertyName("year")] + public int? Year { get; set; } + [JsonIgnore] + public List EpisodeList { get; set; } + + public int TotalMinutes + { + get + { + if (string.IsNullOrEmpty(Time)) + { + return 0; + } + + var arr = Time.Split(":"); + if (arr.Length == 2) + { + return (int)Math.Ceiling((arr[0].ToDouble() * 60 + arr[1].ToDouble()) / 60); + } + if (arr.Length == 3) + { + return (int)Math.Ceiling((arr[0].ToDouble() * 3600 + arr[1].ToDouble() * 60 + arr[2].ToDouble()) / 60); + } + + return 0; + } + } +} + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvVideoInfoResult.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvVideoInfoResult.cs new file mode 100644 index 0000000..4f5f33f --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Entity/MgtvVideoInfoResult.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Entity; + +public class MgtvVideoInfoResult +{ + [JsonPropertyName("data")] + public MgtvVideoInfoData Data { get; set; } + +} + +public class MgtvVideoInfoData +{ + [JsonPropertyName("info")] + public MgtvVideo Info { get; set; } +} \ No newline at end of file diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/ExternalId/EpisodeExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/ExternalId/EpisodeExternalId.cs new file mode 100644 index 0000000..8a9d2d2 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/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.Mgtv.ExternalId +{ + /// + public class EpisodeExternalId : IExternalId + { + /// + public string ProviderName => Mgtv.ScraperProviderName; + + /// + public string Key => Mgtv.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Episode; + + /// + public string UrlFormatString => "#"; + + /// + public bool Supports(IHasProviderIds item) => item is Episode; + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/ExternalId/MovieExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/ExternalId/MovieExternalId.cs new file mode 100644 index 0000000..c6094e5 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/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.Mgtv.ExternalId +{ + /// + public class MovieExternalId : IExternalId + { + /// + public string ProviderName => Mgtv.ScraperProviderName; + + /// + public string Key => Mgtv.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => null; + + /// + public string UrlFormatString => "https://www.mgtv.com/h/{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Movie; + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/ExternalId/SeasonExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/ExternalId/SeasonExternalId.cs new file mode 100644 index 0000000..442cc42 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/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.Mgtv.ExternalId +{ + + /// + public class SeasonExternalId : IExternalId + { + /// + public string ProviderName => Mgtv.ScraperProviderName; + + /// + public string Key => Mgtv.ScraperProviderId; + + /// + public ExternalIdMediaType? Type => null; + + /// + public string UrlFormatString => "https://www.mgtv.com/h/{0}.html"; + + /// + public bool Supports(IHasProviderIds item) => item is Season; + } + +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs new file mode 100644 index 0000000..b167e90 --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/Mgtv.cs @@ -0,0 +1,240 @@ +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.Mgtv.Entity; +using MediaBrowser.Controller.Library; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv; + +public class Mgtv : AbstractScraper +{ + public const string ScraperProviderName = "芒果TV"; + public const string ScraperProviderId = "MgtvID"; + + private readonly MgtvApi _api; + private readonly ILibraryManager _libraryManager; + + public Mgtv(ILoggerFactory logManager, ILibraryManager libraryManager) + : base(logManager.CreateLogger()) + { + _api = new MgtvApi(logManager); + _libraryManager = libraryManager; + } + + public override int DefaultOrder => 6; + + public override bool DefaultEnable => false; + + public override string Name => "芒果TV"; + + 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 = $"{id},{video.EpisodeList[0].VideoId}"; + } + if (video.EpisodeList != null && video.EpisodeList.Count > 0) + { + foreach (var ep in video.EpisodeList) + { + media.Episodes.Add(new ScraperEpisode() { Id = $"{ep.VideoId}", CommentId = $"{id},{ep.VideoId}" }); + } + } + + 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 = $"{id},{video.EpisodeList[0].VideoId}" }; + } + + + // 从季信息元数据中,获取cid值 + // 没SXX季文件夹时,GetParent是Series,有时,GetParent是Season,所以需要通过seasonId中获取 + var seasonId = ((MediaBrowser.Controller.Entities.TV.Episode)item).FindSeasonId(); + var season = _libraryManager.GetItemById(seasonId); + season.ProviderIds.TryGetValue(ScraperProviderId, out var cid); + return new ScraperEpisode() { Id = id, CommentId = $"{cid},{id}" }; + } + + public override async Task GetDanmuContent(BaseItem item, string commentId) + { + if (string.IsNullOrEmpty(commentId)) + { + return null; + } + + var arr = commentId.Split(","); + if (arr.Length < 2) + { + return null; + } + + var cid = arr[0]; + var vid = arr[1]; + if (string.IsNullOrEmpty(cid) || string.IsNullOrEmpty(vid)) + { + return null; + } + var comments = await _api.GetDanmuContentAsync(cid, vid, CancellationToken.None).ConfigureAwait(false); + var danmaku = new ScraperDanmaku(); + danmaku.ChatId = vid.ToLong(); + danmaku.ChatServer = "galaxy.bz.mgtv.com"; + foreach (var comment in comments) + { + + var danmakuText = new ScraperDanmakuText(); + danmakuText.Progress = comment.Time; + danmakuText.Mode = 1; + danmakuText.MidHash = $"[mgtv]{comment.Uuid}"; + danmakuText.Id = comment.Id; + danmakuText.Content = comment.Content; + if (comment.Color != null && comment.Color.ColorLeft != null) + { + danmakuText.Color = comment.Color.ColorLeft.HexNumber; + } + + danmaku.Items.Add(danmakuText); + } + + return danmaku; + } + + + private string NormalizeSearchName(string name) + { + // 去掉可能存在的季名称 + return Regex.Replace(name, @"\s*第.季", ""); + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/MgtvApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/MgtvApi.cs new file mode 100644 index 0000000..fb263cd --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Mgtv/MgtvApi.cs @@ -0,0 +1,184 @@ +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.Mgtv.Entity; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using RateLimiter; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Mgtv; + +public class MgtvApi : AbstractApi +{ + private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); + private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100)); + + /// + /// Initializes a new instance of the class. + /// + /// The . + public MgtvApi(ILoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) + { + httpClient.DefaultRequestHeaders.Add("referer", "https://www.mgtv.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(); + + keyword = HttpUtility.UrlEncode(keyword); + var url = $"https://mobileso.bz.mgtv.com/msite/search/v2?q={keyword}&pc=30&pn=1&sort=-99&ty=0&du=0&pt=0&corr=1&abroad=0&_support=10000000000000000"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = new List(); + var searchResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (searchResult != null && searchResult.Data != null && searchResult.Data.Contents != null) + { + foreach (var content in searchResult.Data.Contents) + { + if (content.Type != "media") + { + continue; + } + foreach (var item in content.Data) + { + + + result.Add(item); + } + } + } + + _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 month = ""; + var idx = 0; + var total = 0; + var videoInfo = new MgtvVideo() { Id = id }; + var list = new List(); + do + { + var url = $"https://pcweb.api.mgtv.com/variety/showlist?allowedRC=1&collection_id={id}&month={month}&page=1&_support=10000000"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (result != null && result.Data != null && result.Data.List != null) + { + list.AddRange(result.Data.List.Where(x => x.SourceClipId == id)); + + total = result.Data.Tabs.Count; + idx++; + month = idx < total ? result.Data.Tabs[idx].Month : ""; + } + + // 等待一段时间避免api请求太快 + await _delayExecuteConstraint; + } while (idx < total && !string.IsNullOrEmpty(month)); + + videoInfo.EpisodeList = list.OrderBy(x => x.VideoId).ToList(); + _memoryCache.Set(cacheKey, videoInfo, expiredOption); + return videoInfo; + } + + + + + public async Task> GetDanmuContentAsync(string cid, string vid, CancellationToken cancellationToken) + { + var danmuList = new List(); + if (string.IsNullOrEmpty(vid)) + { + return danmuList; + } + + // 取视频总时间 + var url = $"https://pcweb.api.mgtv.com/video/info?allowedRC=1&cid={cid}&vid={vid}&change=3&datatype=1&type=1&_support=10000000"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var videoInfoResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (videoInfoResult == null || videoInfoResult.Data == null || videoInfoResult.Data.Info == null) + { + return danmuList; + } + + url = $"https://galaxy.bz.mgtv.com/getctlbarrage?version=3.0.0&vid={vid}&abroad=0&pid=0&os&uuid&deviceid=00000000-0000-0000-0000-000000000000&cid={cid}&ticket&mac&platform=0&appVersion=3.0.0&reqtype=form-post&allowedRC=1"; + response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (result != null && result.Data != null) + { + var idx = 0; + var total = videoInfoResult.Data.Info.TotalMinutes; + do + { + var segmentUrl = $"https://{result.Data.CdnHost}/{result.Data.CdnVersion}/{idx}.json"; + 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.Data != null && segmentResult.Data.Items != null) + { + // 60秒每segment,为避免弹幕太大,从中间隔抽取最大60秒200条弹幕 + danmuList.AddRange(segmentResult.Data.Items.ExtractToNumber(200)); + } + + idx++; + // 等待一段时间避免api请求太快 + await _delayExecuteConstraint; + } while (idx < total); + } + + return danmuList; + } + + protected async Task LimitRequestFrequently() + { + await this._timeConstraint; + } + +} + diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs index ecfcaae..fcb24a3 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs @@ -30,7 +30,7 @@ public class Tencent : AbstractScraper _api = new TencentApi(logManager); } - public override int DefaultOrder => 3; + public override int DefaultOrder => 5; public override bool DefaultEnable => false; diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs index 397ed64..03819ab 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs @@ -21,23 +21,10 @@ 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. /// diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs index c2ccc7b..4e0706b 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Youku/YoukuApi.cs @@ -21,14 +21,9 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Youku; public class YoukuApi : 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)); diff --git a/README.md b/README.md index e987360..966dd32 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Danmu](https://img.shields.io/badge/jellyfin-10.8.x-lightgrey?logo=jellyfin)](https://github.com/cxfksword/jellyfin-plugin-danmu/releases) [![Danmu](https://img.shields.io/github/license/cxfksword/jellyfin-plugin-danmu)](https://github.com/cxfksword/jellyfin-plugin-danmu/main/LICENSE) -jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play,优酷,爱奇艺,腾讯视频。 +jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play,优酷,爱奇艺,腾讯视频,芒果TV。 支持功能: @@ -31,16 +31,14 @@ jellyfin弹幕自动下载插件,已支持的弹幕来源:b站,弹弹play 2. 进入`控制台 -> 媒体库`,点击任一媒体库进入配置页,在最下面的`字幕下载`选项中勾选**Danmu**,并保存 - - 假如想修正匹配错误的弹幕,请在电影或剧集中使用jellyfin的**修改字幕**功能 3. 新加入的影片会自动获取弹幕(只匹配番剧和电影视频),旧影片可以通过计划任务**扫描媒体库匹配弹幕**手动执行获取 -4. 可以在元数据中手动指定匹配的视频ID,如播放链接`https://www.bilibili.com/bangumi/play/ep682965`,对应的视频ID就是`682965` -5. 对于电视剧和动画,可以在元数据中指定季ID,如播放链接`https://www.bilibili.com/bangumi/play/ss1564`,对应的季ID就是`1564`,只要集数和b站的集数的一致,并正确填写了集号,每季视频的弹幕会自动获取 +4. 假如弹幕匹配错误,请在电影或剧集中使用**修改字幕**功能搜索修正 +5. 对于电视剧或动画,需要保证每季视频集数一致,并正确填写集号,这样每季视频的弹幕才会自动获取 6. 同时生成ass弹幕,需要在插件配置中打开,默认是关闭的 7. 定时更新需要自己到计划任务中添加定时时间,默认手工执行更新 -> 电影或季元数据也支持手动指定BV号,来匹配UP主上传的视频弹幕。多P视频和剧集是按顺序一一对应匹配的,所以保证jellyfin中剧集有正确的集号很重要 +> B站电影或季元数据也支持手动指定BV号,来匹配UP主上传的视频弹幕。多P视频和剧集是按顺序一一对应匹配的,所以保证jellyfin中剧集有正确的集号很重要 ## 支持的api接口