Merge pull request #14 from cxfksword/mgtv

Add mgtv danmu
This commit is contained in:
cxfksword
2023-02-19 16:19:03 +08:00
committed by GitHub
22 changed files with 1137 additions and 41 deletions

View File

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

View File

@@ -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<ILibraryManager>();
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
Name = "虚颜"
};
var list = new List<LibraryEvent>();
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<ILibraryManager>();
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
Name = "虚颜",
ProviderIds = new Dictionary<string, string>() { { Mgtv.ScraperProviderId, "519236" } },
};
var list = new List<LibraryEvent>();
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<ILibraryManager>();
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Mgtv.Mgtv(loggerFactory, libraryManagerStub.Object));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Season
{
Name = "大侦探 第八季",
ProductionYear = 2023,
};
var list = new List<LibraryEvent>();
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<ILibraryManager>();
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();
}
}
}

View File

@@ -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)
{

View File

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

View File

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

View File

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

View File

@@ -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<MgtvComment> Items { get; set; }
}

View File

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

View File

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

View File

@@ -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<MgtvEpisodeListTab> Tabs { get; set; }
[JsonPropertyName("pageNo")]
public int PageNo { get; set; }
[JsonPropertyName("pageSize")]
public int PageSize { get; set; }
[JsonPropertyName("list")]
public List<MgtvEpisode> List { get; set; }
}
public class MgtvEpisodeListTab
{
[JsonPropertyName("m")]
public string Month { get; set; }
}

View File

@@ -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<MgtvSearchContent> Contents { get; set; }
}
public class MgtvSearchContent
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("data")]
public List<MgtvSearchItem> 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<string> 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;
}
}
}

View File

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

View File

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

View File

@@ -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
{
/// <inheritdoc />
public class EpisodeExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => Mgtv.ScraperProviderName;
/// <inheritdoc />
public string Key => Mgtv.ScraperProviderId;
/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
/// <inheritdoc />
public string UrlFormatString => "#";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Episode;
}
}

View File

@@ -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
{
/// <inheritdoc />
public class MovieExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => Mgtv.ScraperProviderName;
/// <inheritdoc />
public string Key => Mgtv.ScraperProviderId;
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.mgtv.com/h/{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Movie;
}
}

View File

@@ -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
{
/// <inheritdoc />
public class SeasonExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => Mgtv.ScraperProviderName;
/// <inheritdoc />
public string Key => Mgtv.ScraperProviderId;
/// <inheritdoc />
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
public string UrlFormatString => "https://www.mgtv.com/h/{0}.html";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Season;
}
}

View File

@@ -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<Mgtv>())
{
_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<List<ScraperSearchInfo>> Search(BaseItem item)
{
var list = new List<ScraperSearchInfo>();
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<string?> 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<ScraperMedia?> 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<ScraperEpisode?> 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<ScraperDanmaku?> 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*第.季", "");
}
}

View File

@@ -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));
/// <summary>
/// Initializes a new instance of the <see cref="MgtvApi"/> class.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public MgtvApi(ILoggerFactory loggerFactory)
: base(loggerFactory.CreateLogger<MgtvApi>())
{
httpClient.DefaultRequestHeaders.Add("referer", "https://www.mgtv.com/");
}
public async Task<List<MgtvSearchItem>> SearchAsync(string keyword, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(keyword))
{
return new List<MgtvSearchItem>();
}
var cacheKey = $"search_{keyword}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) };
if (_memoryCache.TryGetValue<List<MgtvSearchItem>>(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<MgtvSearchItem>();
var searchResult = await response.Content.ReadFromJsonAsync<MgtvSearchResult>(_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<List<MgtvSearchItem>>(cacheKey, result, expiredOption);
return result;
}
public async Task<MgtvVideo?> 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<MgtvVideo?>(cacheKey, out var video))
{
return video;
}
var month = "";
var idx = 0;
var total = 0;
var videoInfo = new MgtvVideo() { Id = id };
var list = new List<MgtvEpisode>();
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<MgtvEpisodeListResult>(_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<MgtvVideo?>(cacheKey, videoInfo, expiredOption);
return videoInfo;
}
public async Task<List<MgtvComment>> GetDanmuContentAsync(string cid, string vid, CancellationToken cancellationToken)
{
var danmuList = new List<MgtvComment>();
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<MgtvVideoInfoResult>(_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<MgtvCommentResult>(_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<MgtvCommentSegmentResult>(_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;
}
}

View File

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

View File

@@ -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(@"<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));
private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100));
protected string _cna = string.Empty;
protected string _token = string.Empty;
protected string _tokenEnc = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="TencentApi"/> class.
/// </summary>

View File

@@ -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(@"<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));
private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100));

View File

@@ -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**,并保存
<img src="doc/tutorial.png" width="720px" />
假如想修正匹配错误的弹幕请在电影或剧集中使用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接口