mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-02-03 02:04:47 +08:00
Support tencent danmu
This commit is contained in:
22
Jellyfin.Plugin.Danmu.Test/BaseTest.cs
Normal file
22
Jellyfin.Plugin.Danmu.Test/BaseTest.cs
Normal file
@@ -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 ";
|
||||
}));
|
||||
}
|
||||
}
|
||||
77
Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs
Normal file
77
Jellyfin.Plugin.Danmu.Test/TencentApiTest.cs
Normal file
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
147
Jellyfin.Plugin.Danmu.Test/TencentTest.cs
Normal file
147
Jellyfin.Plugin.Danmu.Test/TencentTest.cs
Normal file
@@ -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<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
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 scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Tencent.Tencent(loggerFactory));
|
||||
|
||||
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
|
||||
|
||||
var item = new Movie
|
||||
{
|
||||
Name = "少年派的奇幻漂流",
|
||||
ProviderIds = new Dictionary<string, string>() { { Tencent.ScraperProviderId, "19rrjv4kz0" } },
|
||||
};
|
||||
|
||||
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 scraperManager = new ScraperManager(loggerFactory);
|
||||
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Tencent.Tencent(loggerFactory));
|
||||
|
||||
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
|
||||
var directoryServiceStub = new Mock<IDirectoryService>();
|
||||
var libraryManagerStub = new Mock<ILibraryManager>();
|
||||
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 api = new Tencent(loggerFactory);
|
||||
var media = await api.GetMedia(new Season(), "mzc002006n62s11");
|
||||
Console.WriteLine(media);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 () =>
|
||||
|
||||
21
Jellyfin.Plugin.Danmu/Core/CanIgnoreException.cs
Normal file
21
Jellyfin.Plugin.Danmu/Core/CanIgnoreException.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Core;
|
||||
|
||||
class CanIgnoreException : Exception
|
||||
{
|
||||
public CanIgnoreException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Don't display call stack as it's irrelevant
|
||||
/// </summary>
|
||||
public override string StackTrace
|
||||
{
|
||||
get
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,28 @@ namespace Jellyfin.Plugin.Danmu.Core.Extensions
|
||||
public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)
|
||||
=> self.Select((item, index) => (item, index));
|
||||
|
||||
/// <summary>
|
||||
/// 从list抽取间隔指定大小数量的item
|
||||
/// </summary>
|
||||
public static IEnumerable<T> ExtractToNumber<T>(this IEnumerable<T> self, int limit)
|
||||
{
|
||||
|
||||
var count = self.Count();
|
||||
var step = (int)Math.Ceiling((double)count / limit);
|
||||
var list = new List<T>();
|
||||
var idx = 0;
|
||||
for (var i = 0; i < limit; i++)
|
||||
{
|
||||
if (idx >= count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
list.Add(self.ElementAt(idx));
|
||||
idx += step;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
18
Jellyfin.Plugin.Danmu/Core/Extensions/UriExtension.cs
Normal file
18
Jellyfin.Plugin.Danmu/Core/Extensions/UriExtension.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<HttpResponseMessage> SendAsync(
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<BilibiliApi> _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
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public BilibiliApi(ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory.CreateLogger<BilibiliApi>())
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<BilibiliApi>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
/// <summary>
|
||||
/// 出现时间(单位ms)
|
||||
/// </summary>
|
||||
public int Progress { get; set; }
|
||||
/// <summary>
|
||||
/// 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
|
||||
/// </summary>
|
||||
public int Mode { get; set; }
|
||||
public int Fontsize { get; set; } = 25; //文字大小
|
||||
public uint Color { get; set; } //弹幕颜色
|
||||
public string MidHash { get; set; } //发送者UID的HASH
|
||||
|
||||
@@ -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(@"<a.*?h5-show-card.*?>([\w\W]+?)</a>", 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<IqiyiApi>())
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<long, TencentCommentSegment> SegmentIndex { get; set; }
|
||||
}
|
||||
|
||||
public class TencentCommentSegment
|
||||
{
|
||||
[JsonPropertyName("segment_name")]
|
||||
public string SegmentName { get; set; }
|
||||
[JsonPropertyName("segment_start")]
|
||||
public string SegmentStart { get; set; }
|
||||
}
|
||||
@@ -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<TencentComment> BarrageList { get; set; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<TencentModuleList> ModuleListDatas { get; set; }
|
||||
}
|
||||
|
||||
public class TencentModuleList
|
||||
{
|
||||
[JsonPropertyName("module_datas")]
|
||||
public List<TencentModule> ModuleDatas { get; set; }
|
||||
}
|
||||
|
||||
public class TencentModule
|
||||
{
|
||||
[JsonPropertyName("item_data_lists")]
|
||||
public TencentModuleItemList ItemDataLists { get; set; }
|
||||
}
|
||||
|
||||
public class TencentModuleItemList
|
||||
{
|
||||
[JsonPropertyName("item_datas")]
|
||||
public List<TencentModuleItem> 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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<TencentSearchItem> ItemList { get; set; }
|
||||
}
|
||||
|
||||
public class TencentSearchItem
|
||||
{
|
||||
[JsonPropertyName("doc")]
|
||||
public TencentSearchDoc Doc { get; set; }
|
||||
[JsonPropertyName("videoInfo")]
|
||||
public TencentVideo VideoInfo { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TencentEpisode> EpisodeList { get; set; }
|
||||
}
|
||||
|
||||
public class TencentSubjectDoc
|
||||
{
|
||||
[JsonPropertyName("videoNum")]
|
||||
public int VideoNum { get; set; }
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class EpisodeExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Tencent.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Tencent.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "#";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Episode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.ExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class MovieExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Tencent.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Tencent.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://v.qq.com/x/cover/{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Movie;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Danmu.Scrapers.Tencent.ExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SeasonExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => Tencent.ScraperProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => Tencent.ScraperProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string UrlFormatString => "https://v.qq.com/x/cover/{0}.html";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Season;
|
||||
}
|
||||
|
||||
}
|
||||
243
Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs
Normal file
243
Jellyfin.Plugin.Danmu/Scrapers/Tencent/Tencent.cs
Normal file
@@ -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<Tencent>())
|
||||
{
|
||||
_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<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 = $"{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<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 = $"{video.EpisodeList[0].Vid}" };
|
||||
}
|
||||
|
||||
return new ScraperEpisode() { Id = id, CommentId = id };
|
||||
}
|
||||
|
||||
public override async Task<ScraperDanmaku?> 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<TencentCommentContentStyle>();
|
||||
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*第.季", "");
|
||||
}
|
||||
}
|
||||
178
Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs
Normal file
178
Jellyfin.Plugin.Danmu/Scrapers/Tencent/TencentApi.cs
Normal file
@@ -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(@"<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>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public TencentApi(ILoggerFactory loggerFactory)
|
||||
: base(loggerFactory.CreateLogger<TencentApi>())
|
||||
{
|
||||
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<List<TencentVideo>> SearchAsync(string keyword, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyword))
|
||||
{
|
||||
return new List<TencentVideo>();
|
||||
}
|
||||
|
||||
var cacheKey = $"search_{keyword}";
|
||||
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) };
|
||||
if (_memoryCache.TryGetValue<List<TencentVideo>>(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<TencentSearchRequest>(url, postData, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = new List<TencentVideo>();
|
||||
var searchResult = await response.Content.ReadFromJsonAsync<TencentSearchResult>(_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<List<TencentVideo>>(cacheKey, result, expiredOption);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TencentVideo?> 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<TencentVideo?>(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<TencentEpisodeListRequest>(url, postData, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<TencentEpisodeListResult>(_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<TencentVideo?>(cacheKey, videoInfo, expiredOption);
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
_memoryCache.Set<TencentVideo?>(cacheKey, null, expiredOption);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public async Task<List<TencentComment>> GetDanmuContentAsync(string vid, CancellationToken cancellationToken)
|
||||
{
|
||||
var danmuList = new List<TencentComment>();
|
||||
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<TencentCommentResult>(_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<TencentCommentSegmentResult>(_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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(@"<a.*?h5-show-card.*?>([\w\W]+?)</a>", 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<YoukuApi>())
|
||||
{
|
||||
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<YoukuCommentResult>(result.Data.Result);
|
||||
if (commentResult != null && commentResult.Data != null)
|
||||
{
|
||||
return commentResult.Data.Result;
|
||||
// 每段有60秒弹幕,为避免弹幕太大,从中间隔抽取最大60秒200条弹幕
|
||||
return commentResult.Data.Result.ExtractToNumber(200).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user