diff --git a/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs b/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs index fe572dd..c450313 100644 --- a/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs +++ b/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs @@ -51,9 +51,10 @@ namespace Jellyfin.Plugin.Danmu.Test { try { - var vid = "3493131456125200"; // 电视剧 - // var vid = "429872"; // 电影 - var result = await api.GetVideoAsync(vid, CancellationToken.None); + // var id = "j5998jke4w"; // 综艺 + // var id = "xkt6z3z798"; // 电视剧 + var id = "1e54n0pt5ro"; // 电影 + var result = await api.GetVideoAsync(id, CancellationToken.None); Console.WriteLine(result); } catch (Exception ex) diff --git a/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs b/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs index a60fea3..633aca0 100644 --- a/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs +++ b/Jellyfin.Plugin.Danmu.Test/IqiyiTest.cs @@ -42,7 +42,8 @@ namespace Jellyfin.Plugin.Danmu.Test var item = new Movie { - Name = "少年派的奇幻漂流" + Name = "无间道3:终极无间", + ProductionYear = 2003 }; var list = new List(); @@ -134,6 +135,7 @@ namespace Jellyfin.Plugin.Danmu.Test } + [TestMethod] public void TestGetMedia() { @@ -143,7 +145,7 @@ namespace Jellyfin.Plugin.Danmu.Test try { var api = new Iqiyi(loggerFactory); - var media = await api.GetMedia(new Season(), "19rrhaen1h"); + var media = await api.GetMedia(new Season(), "19rrmacgqs"); Console.WriteLine(media); } catch (Exception ex) @@ -163,7 +165,7 @@ namespace Jellyfin.Plugin.Danmu.Test try { var api = new Iqiyi(loggerFactory); - var media = await api.GetMedia(new Season(), "25apmxdhmmh"); + var media = await api.GetMedia(new Season(), "1m5gylxxqu0"); Console.WriteLine(media); } catch (Exception ex) diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiHtmlAlbumInfo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiHtmlAlbumInfo.cs new file mode 100644 index 0000000..f67797a --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiHtmlAlbumInfo.cs @@ -0,0 +1,24 @@ +using System.Text.RegularExpressions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiHtmlAlbumInfo + { + [JsonPropertyName("qipuId")] + public Int64 AlbumId { get; set; } + [JsonPropertyName("albumName")] + public string AlbumName { get; set; } + [JsonPropertyName("albumPageUrl")] + public string AlbumUrl { get; set; } + [JsonPropertyName("videoCount")] + public int VideoCount { get; set; } + + + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiHtmlVideoInfo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiHtmlVideoInfo.cs new file mode 100644 index 0000000..7bc888f --- /dev/null +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiHtmlVideoInfo.cs @@ -0,0 +1,53 @@ +using System.Text.RegularExpressions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity +{ + public class IqiyiHtmlVideoInfo + { + private static readonly Regex regLinkId = new Regex(@"v_(\w+?)\.html", RegexOptions.Compiled); + + + [JsonPropertyName("albumQipuId")] + public Int64 AlbumId { get; set; } + [JsonPropertyName("tvid")] + public Int64 TvId { get; set; } + [JsonPropertyName("videoName")] + public String VideoName { get; set; } + [JsonPropertyName("videoUrl")] + public String VideoUrl { get; set; } + [JsonPropertyName("channelName")] + public string ChannelName { get; set; } + [JsonPropertyName("duration")] + public int Duration { get; set; } + + [JsonIgnore] + public List Epsodelist { get; set; } + + [JsonIgnore] + public IqiyiHtmlAlbumInfo? AlbumInfo { get; set; } + + [JsonIgnore] + public string LinkId + { + get + { + var match = regLinkId.Match(VideoUrl); + if (match.Success && match.Groups.Count > 1) + { + return match.Groups[1].Value.Trim(); + } + else + { + return null; + } + } + } + + } +} diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchAlbumInfo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchAlbumInfo.cs index 3060c86..fd69aa7 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchAlbumInfo.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchAlbumInfo.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity { public class IqiyiSearchAlbumInfo { - private static readonly Regex regLinkId = new Regex(@"(v_|a_)(\w+?)\.html", RegexOptions.Compiled); + private static readonly Regex regLinkId = new Regex(@"v_(\w+?)\.html", RegexOptions.Compiled); [JsonPropertyName("albumId")] public Int64 AlbumId { get; set; } @@ -66,14 +66,24 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity [JsonPropertyName("videoinfos")] public List VideoInfos { get; set; } - public string LinkId + /// + /// 编码后的视频ID + /// + [JsonIgnore] + public string? LinkId { get { - var match = regLinkId.Match(Link); - if (match.Success && match.Groups.Count > 2) + var link = Link; + if (VideoInfos != null && VideoInfos.Count > 0) { - return match.Groups[2].Value.Trim(); + link = VideoInfos.First().ItemLink; + } + + var match = regLinkId.Match(link); + if (match.Success && match.Groups.Count > 1) + { + return match.Groups[1].Value.Trim(); } else { diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchVideoInfo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchVideoInfo.cs index a0b2c36..f34d1e0 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchVideoInfo.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiSearchVideoInfo.cs @@ -13,5 +13,8 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity { [JsonPropertyName("tvId")] public Int64 VideoId { get; set; } + + [JsonPropertyName("itemLink")] + public string ItemLink { get; set; } } } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs index 7978eca..6b37b67 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Entity/IqiyiVideo.cs @@ -10,8 +10,6 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity { public class IqiyiVideo { - private static readonly Regex regLinkId = new Regex(@"(v_|a_)(\w+?)\.html", RegexOptions.Compiled); - [JsonPropertyName("albumId")] public Int64 AlbumId { get; set; } [JsonPropertyName("tvId")] @@ -34,23 +32,5 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.Entity [JsonPropertyName("epsodelist")] public List Epsodelist { get; set; } - - public string LinkId - { - get - { - if (string.IsNullOrEmpty(AlbumUrl)) return null; - - var match = regLinkId.Match(AlbumUrl); - if (match.Success && match.Groups.Count > 2) - { - return match.Groups[2].Value.Trim(); - } - else - { - return null; - } - } - } } } diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs index 68a45dc..57097f1 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/ExternalId/SeasonExternalId.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi.ExternalId public ExternalIdMediaType? Type => null; /// - public string UrlFormatString => "https://www.iqiyi.com/a_{0}.html"; + public string UrlFormatString => "https://www.iqiyi.com/v_{0}.html"; /// public bool Supports(IHasProviderIds item) => item is Season; diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs index 623d0e7..d9a6f17 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/Iqiyi.cs @@ -47,10 +47,6 @@ public class Iqiyi : AbstractScraper var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false); foreach (var video in videos) { - var videoId = video.VideoId; - var title = video.Name; - var pubYear = video.Year; - if (isMovieItemType && video.ChannelName != "电影") { continue; @@ -64,9 +60,9 @@ public class Iqiyi : AbstractScraper list.Add(new ScraperSearchInfo() { Id = $"{video.LinkId}", - Name = title, + Name = video.Name, Category = video.ChannelName, - Year = pubYear, + Year = video.Year, EpisodeSize = video.ItemTotalNumber, }); } @@ -82,7 +78,6 @@ public class Iqiyi : AbstractScraper var videos = await this._api.SearchAsync(searchName, CancellationToken.None).ConfigureAwait(false); foreach (var video in videos) { - var videoId = video.VideoId; var title = video.Name; var pubYear = video.Year; @@ -126,15 +121,9 @@ public class Iqiyi : AbstractScraper return null; } - // id是编码后的,需要还原为真实id + // id是编码后的 var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie; - var tvId = await _api.GetTvId(id, !isMovieItemType, CancellationToken.None); - if (string.IsNullOrEmpty(tvId)) - { - return null; - } - - var video = await _api.GetVideoAsync(tvId, CancellationToken.None).ConfigureAwait(false); + var video = await _api.GetVideoAsync(id, CancellationToken.None).ConfigureAwait(false); if (video == null) { log.LogInformation("[{0}]获取不到视频信息:id={1}", this.Name, id); @@ -143,7 +132,7 @@ public class Iqiyi : AbstractScraper var media = new ScraperMedia(); - media.Id = video.LinkId; // 使用url编码后的id,movie使用vid,电视剧使用aid + media.Id = id; // 使用url编码后的id if (isMovieItemType && video.Epsodelist != null && video.Epsodelist.Count > 0) { media.CommentId = $"{video.Epsodelist[0].TvId}"; @@ -167,14 +156,14 @@ public class Iqiyi : AbstractScraper return null; } - // id是编码后的,需要还原为真实id - var tvId = await _api.GetTvId(id, false, CancellationToken.None); - if (string.IsNullOrEmpty(tvId)) + // id是编码后的 + var video = await _api.GetVideoBaseAsync(id, CancellationToken.None).ConfigureAwait(false); + if (video == null) { return null; } - return new ScraperEpisode() { Id = id, CommentId = tvId }; + return new ScraperEpisode() { Id = id, CommentId = $"{video.TvId}" }; } public override async Task GetDanmuContent(BaseItem item, string commentId) diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs index 3ad4fc7..c2476a2 100644 --- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs +++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs @@ -25,7 +25,9 @@ namespace Jellyfin.Plugin.Danmu.Scrapers.Iqiyi; public class IqiyiApi : AbstractApi { - private static readonly Regex regTvId = new Regex(@"""tvid"":(\d+?),", RegexOptions.Compiled); + private new 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 Regex regVideoInfo = new Regex(@"""videoInfo"":(\{.+?\}),", RegexOptions.Compiled); + private static readonly Regex regAlbumInfo = new Regex(@"""albumInfo"":(\{.+?\}),", RegexOptions.Compiled); private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000)); @@ -42,6 +44,7 @@ public class IqiyiApi : AbstractApi public IqiyiApi(ILoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { + httpClient.DefaultRequestHeaders.Add("user-agent", HTTP_USER_AGENT); } @@ -70,7 +73,6 @@ public class IqiyiApi : AbstractApi var searchResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); if (searchResult != null && searchResult.Data != null) { - // 综艺没找到接口获取准确的正片列表,忽略不处理 result = searchResult.Data.DocInfos .Where(x => x.Score > 0.7) .Select(x => x.AlbumDocInfo) @@ -82,7 +84,7 @@ public class IqiyiApi : AbstractApi return result; } - public async Task GetVideoAsync(string id, CancellationToken cancellationToken) + public async Task GetVideoAsync(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { @@ -91,42 +93,73 @@ public class IqiyiApi : AbstractApi var cacheKey = $"video_{id}"; var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; - if (_memoryCache.TryGetValue(cacheKey, out var video)) + if (_memoryCache.TryGetValue(cacheKey, out var video)) { return video; } - // 获取影片信息(vid):https://pcw-api.iqiyi.com/video/video/baseinfo/429872 // 获取电视剧信息(aid):https://pcw-api.iqiyi.com/album/album/baseinfo/5328486914190101 - // 获取电视剧剧集信息(综艺不适用)(aid):https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid=5328486914190101&page=1&size=100 + // 获取电视剧剧集信息(综艺不适用)(aid):https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid=5328486914190101&page=1&size=10 // 获取电视剧剧集信息(综艺不适用)(aid):https://pub.m.iqiyi.com/h5/main/videoList/album/?albumId=5328486914190101&size=39&page=1&needPrevue=true&needVipPrevue=false - var url = $"https://pcw-api.iqiyi.com/video/video/baseinfo/{id}"; - 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) + var videoInfo = await GetVideoBaseAsync(id, cancellationToken).ConfigureAwait(false); + if (videoInfo != null) { - if (result.Data.ChannelId == 6) + if (videoInfo.ChannelName == "综艺" && videoInfo.AlbumInfo != null) { // 综艺需要特殊处理 - result.Data.Epsodelist = await this.GetZongyiEpisodesAsync($"{result.Data.AlbumId}", cancellationToken).ConfigureAwait(false); + videoInfo.Epsodelist = await this.GetZongyiEpisodesAsync($"{videoInfo.AlbumInfo.AlbumId}", cancellationToken).ConfigureAwait(false); } - else if (result.Data.ChannelId != 1) + else if (videoInfo.ChannelName != "电影" && videoInfo.AlbumInfo != null) { // 电视剧需要再获取剧集信息 - result.Data.Epsodelist = await this.GetEpisodesAsync($"{result.Data.AlbumId}", result.Data.VideoCount, cancellationToken).ConfigureAwait(false); + videoInfo.Epsodelist = await this.GetEpisodesAsync($"{videoInfo.AlbumId}", videoInfo.AlbumInfo.VideoCount, cancellationToken).ConfigureAwait(false); } - else + else if (videoInfo.ChannelName == "电影") { // 电影 - result.Data.Epsodelist = new List() { - new IqiyiEpisode() {TvId = result.Data.TvId, Order = 1, Name = result.Data.Name, Duration = result.Data.Duration, PlayUrl = result.Data.PlayUrl} + var duration = new TimeSpan(0, 0, videoInfo.Duration); + videoInfo.Epsodelist = new List() { + new IqiyiEpisode() {TvId = videoInfo.TvId, Order = 1, Name = videoInfo.VideoName, Duration = duration.ToString(@"hh\:mm\:ss"), PlayUrl = videoInfo.VideoUrl} }; } - _memoryCache.Set(cacheKey, result.Data, expiredOption); - return result.Data; + _memoryCache.Set(cacheKey, videoInfo, expiredOption); + return videoInfo; } - _memoryCache.Set(cacheKey, null, expiredOption); + _memoryCache.Set(cacheKey, null, expiredOption); + return null; + } + + public async Task GetVideoBaseAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + var cacheKey = $"video_base_{id}"; + var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; + if (_memoryCache.TryGetValue(cacheKey, out var video)) + { + return video; + } + + await this.LimitRequestFrequently(); + + var url = $"https://m.iqiyi.com/v_{id}.html"; + var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var body = await response.Content.ReadAsStringAsync(); + var videoJson = regVideoInfo.FirstMatchGroup(body); + var videoInfo = videoJson.FromJson(); + if (videoInfo != null) + { + var albumJson = regAlbumInfo.FirstMatchGroup(body); + videoInfo.AlbumInfo = albumJson.FromJson(); + _memoryCache.Set(cacheKey, videoInfo, expiredOption); + return videoInfo; + } + + _memoryCache.Set(cacheKey, null, expiredOption); return null; } @@ -163,8 +196,6 @@ public class IqiyiApi : AbstractApi return new List(); } - - // 获取电视剧剧集信息(综艺不适用) var url = $"https://pcw-api.iqiyi.com/album/album/baseinfo/{albumId}"; var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); @@ -193,7 +224,7 @@ public class IqiyiApi : AbstractApi var videoListResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false); if (videoListResult != null && videoListResult.Data != null && videoListResult.Data.Videos != null && videoListResult.Data.Videos.Count > 0) { - list.AddRange(videoListResult.Data.Videos); + list.AddRange(videoListResult.Data.Videos.Where(x => !x.ShortTitle.Contains("精编版") && !x.ShortTitle.Contains("会员版"))); } else { @@ -215,25 +246,6 @@ public class IqiyiApi : AbstractApi return new List(); } - public async Task GetTvId(string id, bool isAlbum, CancellationToken cancellationToken) - { - var cacheKey = $"tvid_{id}"; - var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; - if (_memoryCache.TryGetValue(cacheKey, out var tvid)) - { - return tvid; - } - - var url = isAlbum ? $"https://m.iqiyi.com/a_{id}.html" : $"https://m.iqiyi.com/v_{id}.html"; - var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var body = await response.Content.ReadAsStringAsync(); - tvid = regTvId.FirstMatchGroup(body); - _memoryCache.Set(cacheKey, tvid, expiredOption); - - return tvid; - } public async Task> GetDanmuContentAsync(string tvId, CancellationToken cancellationToken) {