mirror of
https://github.com/91270/MeiamSubtitles.git
synced 2026-05-12 02:56:37 +08:00
feat: 升级调试工具并还原哈希匹配逻辑
1. 调试工具升级:在 DevTool 中实现了射手网和迅雷影音的实时 HASH 计算与 API 接口测试功能。 2. 还原匹配逻辑:确认 Open Subtitles 冲突并非由于 IsHashMatch 引起,现已恢复精准哈希匹配 (IsHashMatch = true) 逻辑。 3. 优化语言识别:完善了 NormalizeLanguage 逻辑,确保支持 zho, chi, zh 等多种语言代码。 4. 日志级别调整:为了方便排查搜索不到字幕的问题,将字幕接口的原始返回内容恢复为 Info 记录级别。
This commit is contained in:
@@ -1,96 +1,211 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Intrinsics.Arm;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace Emby.Subtitle.DevTool
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
private static readonly HttpClient _httpClient = new HttpClient();
|
||||
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
//Console.WriteLine(ComputeFileHash($"X:\\Favorites\\Movie\\八佰 (2020)\\八佰 (2020) 1080p TrueHD.mkv"));
|
||||
Console.WriteLine(ComputeFileHash($"D:\\Documents\\Downloads\\testidx.avi"));
|
||||
// 设置控制台编码为 UTF8 防止中文乱码
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
|
||||
Console.WriteLine("================ MeiamSubtitles 调试工具 ================");
|
||||
|
||||
// 待测试的影音文件路径
|
||||
var testFilePath = @"D:\Source\MeiamSubtitles\TestServer\Movie\2009\三傻大闹宝莱坞\三傻大闹宝莱坞 (2009) - 1080p.mkv";
|
||||
|
||||
if (!File.Exists(testFilePath))
|
||||
{
|
||||
Console.WriteLine($"[错误] 文件不存在: {testFilePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[文件] {testFilePath}");
|
||||
Console.WriteLine("-------------------------------------------------------");
|
||||
|
||||
// 1. 射手网 (Shooter)
|
||||
Console.WriteLine("\n[1/2] 正在请求:射手网 (Shooter)...");
|
||||
var shooterHash = ComputeShooterHash(testFilePath);
|
||||
Console.WriteLine($" > HASH: {shooterHash}");
|
||||
await TestShooterApi(testFilePath, shooterHash);
|
||||
|
||||
// 2. 迅雷影音 (Thunder)
|
||||
Console.WriteLine("\n[2/2] 正在请求:迅雷影音 (Thunder)...");
|
||||
var thunderCid = await GetThunderCidAsync(testFilePath);
|
||||
Console.WriteLine($" > CID: {thunderCid}");
|
||||
await TestThunderApi(testFilePath, thunderCid);
|
||||
|
||||
Console.WriteLine("\n-------------------------------------------------------");
|
||||
Console.WriteLine("调试结束,按任意键退出...");
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
||||
private static string GetCid(string filePath)
|
||||
{
|
||||
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
var reader = new BinaryReader(stream);
|
||||
var fileSize = new FileInfo(filePath).Length;
|
||||
var sha1 = SHA1.Create();
|
||||
var buffer = new byte[0xf000];
|
||||
if (fileSize < 0xf000)
|
||||
{
|
||||
reader.Read(buffer, 0, (int)fileSize);
|
||||
buffer = sha1.ComputeHash(buffer, 0, (int)fileSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read(buffer, 0, 0x5000);
|
||||
stream.Seek(fileSize / 3, SeekOrigin.Begin);
|
||||
reader.Read(buffer, 0x5000, 0x5000);
|
||||
stream.Seek(fileSize - 0x5000, SeekOrigin.Begin);
|
||||
reader.Read(buffer, 0xa000, 0x5000);
|
||||
#region API 测试方法
|
||||
|
||||
buffer = sha1.ComputeHash(buffer, 0, 0xf000);
|
||||
}
|
||||
var result = "";
|
||||
foreach (var i in buffer)
|
||||
private static async Task TestShooterApi(string filePath, string hash)
|
||||
{
|
||||
try
|
||||
{
|
||||
result += string.Format("{0:X2}", i);
|
||||
var url = "https://www.shooter.cn/api/subapi.php";
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
{ "filehash", hash},
|
||||
{ "pathinfo", Path.GetFileName(filePath)},
|
||||
{ "format", "json"},
|
||||
{ "lang", "chn"}
|
||||
};
|
||||
|
||||
var content = new FormUrlEncodedContent(formData);
|
||||
var response = await _httpClient.PostAsync(url, content);
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($" > STATUS: {response.StatusCode}");
|
||||
Console.WriteLine($" > RETURN: {result}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" > ERROR: {ex.Message}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string ComputeFileHash(string filePath)
|
||||
private static async Task TestThunderApi(string filePath, string cid)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 迅雷搜索接口通常基于文件名,CID 用于后续匹配校验
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var url = $"https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name={HttpUtility.UrlEncode(fileName)}";
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Add("User-Agent", "MeiamSub.Thunder");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($" > STATUS: {response.StatusCode}");
|
||||
Console.WriteLine($" > RETURN: {result}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($" > ERROR: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HASH 算法实现
|
||||
|
||||
/// <summary>
|
||||
/// 射手网 HASH 算法
|
||||
/// </summary>
|
||||
public static string ComputeShooterHash(string filePath)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(filePath);
|
||||
if (!fileInfo.Exists || fileInfo.Length < 8 * 1024) return "";
|
||||
|
||||
string ret = "";
|
||||
|
||||
if (!fileInfo.Exists || fileInfo.Length < 8 * 1024)
|
||||
using (FileStream fs = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
long[] offset = new long[4];
|
||||
offset[3] = fileInfo.Length - 8 * 1024;
|
||||
offset[2] = fileInfo.Length / 3;
|
||||
offset[1] = fileInfo.Length / 3 * 2;
|
||||
offset[0] = 4 * 1024;
|
||||
|
||||
string ret = "";
|
||||
byte[] bBuf = new byte[4096];
|
||||
|
||||
using (MD5 md5 = MD5.Create())
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
fs.Seek(offset[i], SeekOrigin.Begin);
|
||||
fs.Read(bBuf, 0, 4096);
|
||||
byte[] data = md5.ComputeHash(bBuf);
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
for (int j = 0; j < data.Length; j++) sBuilder.Append(data[j].ToString("x2"));
|
||||
if (!string.IsNullOrEmpty(ret)) ret += ";";
|
||||
ret += sBuilder.ToString();
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
FileStream fs = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
|
||||
|
||||
long[] offset = new long[4];
|
||||
offset[3] = fileInfo.Length - 8 * 1024;
|
||||
offset[2] = fileInfo.Length / 3;
|
||||
offset[1] = fileInfo.Length / 3 * 2;
|
||||
offset[0] = 4 * 1024;
|
||||
|
||||
byte[] bBuf = new byte[1024 * 4];
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
fs.Seek(offset[i], 0);
|
||||
fs.Read(bBuf, 0, 4 * 1024);
|
||||
|
||||
MD5 md5Hash = MD5.Create();
|
||||
byte[] data = md5Hash.ComputeHash(bBuf);
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
|
||||
for (int j = 0; j < data.Length; j++)
|
||||
{
|
||||
sBuilder.Append(data[j].ToString("x2"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ret))
|
||||
{
|
||||
ret += ";";
|
||||
}
|
||||
|
||||
ret += sBuilder.ToString();
|
||||
}
|
||||
|
||||
fs.Close();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 迅雷 CID 算法 (基于 SHA1)
|
||||
/// </summary>
|
||||
public static async Task<string> GetThunderCidAsync(string filePath)
|
||||
{
|
||||
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous))
|
||||
{
|
||||
var fileSize = new FileInfo(filePath).Length;
|
||||
using (var sha1 = SHA1.Create())
|
||||
{
|
||||
var buffer = new byte[0xf000];
|
||||
if (fileSize < 0xf000)
|
||||
{
|
||||
await stream.ReadAsync(buffer, 0, (int)fileSize);
|
||||
buffer = sha1.ComputeHash(buffer, 0, (int)fileSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
await stream.ReadAsync(buffer, 0, 0x5000);
|
||||
stream.Seek(fileSize / 3, SeekOrigin.Begin);
|
||||
await stream.ReadAsync(buffer, 0x5000, 0x5000);
|
||||
stream.Seek(fileSize - 0x5000, SeekOrigin.Begin);
|
||||
await stream.ReadAsync(buffer, 0xa000, 0x5000);
|
||||
buffer = sha1.ComputeHash(buffer, 0, 0xf000);
|
||||
}
|
||||
var result = "";
|
||||
foreach (var i in buffer) result += string.Format("{0:X2}", i);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// QQ 播放器 VUID 算法
|
||||
/// </summary>
|
||||
public static async Task<string> ComputeQQVuidAsync(string filePath)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(filePath);
|
||||
if (!fileInfo.Exists || fileInfo.Length < 8 * 1024) return "";
|
||||
|
||||
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous))
|
||||
{
|
||||
long[] offsets = new long[3];
|
||||
offsets[0] = 0;
|
||||
offsets[1] = fileInfo.Length / 3;
|
||||
offsets[2] = fileInfo.Length - 8 * 1024;
|
||||
|
||||
StringBuilder combinedMd5 = new StringBuilder();
|
||||
byte[] buffer = new byte[4096];
|
||||
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
fs.Seek(offset, SeekOrigin.Begin);
|
||||
await fs.ReadAsync(buffer, 0, 4096);
|
||||
byte[] hashBytes = md5.ComputeHash(buffer);
|
||||
foreach (byte b in hashBytes) combinedMd5.Append(b.ToString("x2"));
|
||||
}
|
||||
byte[] finalHash = md5.ComputeHash(Encoding.ASCII.GetBytes(combinedMd5.ToString()));
|
||||
StringBuilder result = new StringBuilder();
|
||||
foreach (byte b in finalHash) result.Append(b.ToString("x2"));
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Emby.MeiamSub.Shooter
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public int Order => 1;
|
||||
public int Order => 100;
|
||||
public string Name => "MeiamSub.Shooter";
|
||||
|
||||
/// <summary>
|
||||
@@ -145,17 +145,17 @@ namespace Emby.MeiamSub.Shooter
|
||||
{
|
||||
Url = subFile.Link,
|
||||
Format = subFile.Ext,
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {Path.GetFileName(request.MediaPath)} | {language} | 射手",
|
||||
Language = request.Language,
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = subFile.Ext,
|
||||
Comment = $"Format : {ExtractFormat(subFile.Ext)}",
|
||||
IsHashMatch = true
|
||||
}); }
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {Path.GetFileName(request.MediaPath)} | {request.Language} | 射手",
|
||||
Language = request.Language,
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = subFile.Ext,
|
||||
Comment = $"Format : {ExtractFormat(subFile.Ext)}",
|
||||
IsHashMatch = true
|
||||
}); }
|
||||
}
|
||||
_logger.Info("{0} Search | Summary -> Get {1} Subtitles", new object[2] { Name, remoteSubtitles.Count });
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Emby.MeiamSub.Thunder
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public int Order => 1;
|
||||
public int Order => 100;
|
||||
|
||||
public string Name => "MeiamSub.Thunder";
|
||||
|
||||
@@ -136,17 +136,17 @@ namespace Emby.MeiamSub.Thunder
|
||||
{
|
||||
Url = item.Url,
|
||||
Format = item.Ext,
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {item.Name} | {(item.Langs == string.Empty ? "未知" : item.Langs)} | 迅雷",
|
||||
Language = request.Language,
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = item.Ext,
|
||||
Comment = $"Format : {item.Ext}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
}); }
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {item.Name} | {(item.Langs == string.Empty ? "未知" : item.Langs)} | 迅雷",
|
||||
Language = request.Language,
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = item.Ext,
|
||||
Comment = $"Format : {item.Ext}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
}); }
|
||||
}
|
||||
|
||||
_logger.Info("{0} Search | Summary -> Get {1} Subtitles", new object[2] { Name, remoteSubtitles.Count });
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
|
||||
private const string ApiUrl = "https://www.shooter.cn/api/subapi.php";
|
||||
|
||||
public int Order => 1;
|
||||
public int Order => 100;
|
||||
|
||||
public string Name => "MeiamSub.Shooter";
|
||||
|
||||
|
||||
@@ -1,170 +1,170 @@
|
||||
using Jellyfin.MeiamSub.Thunder.Model;
|
||||
using Jellyfin.MeiamSub.Thunder.Model;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.MeiamSub.Thunder
|
||||
{
|
||||
/// <summary>
|
||||
/// 迅雷看看字幕提供程序
|
||||
/// 负责与迅雷 API 进行交互,通过 CID (Content ID) 匹配并下载字幕。
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// </summary>
|
||||
public class ThunderProvider : ISubtitleProvider, IHasOrder
|
||||
{
|
||||
#region 变量声明
|
||||
public const string ASS = "ass";
|
||||
public const string SSA = "ssa";
|
||||
public const string SRT = "srt";
|
||||
|
||||
private readonly ILogger<ThunderProvider> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private static readonly JsonSerializerOptions _deserializeOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
};
|
||||
|
||||
public int Order => 1;
|
||||
public string Name => "MeiamSub.Thunder";
|
||||
|
||||
/// <summary>
|
||||
/// 支持电影、剧集
|
||||
/// </summary>
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode };
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
public ThunderProvider(ILogger<ThunderProvider> logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger.LogInformation($"{Name} Init");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 查询字幕
|
||||
|
||||
/// <summary>
|
||||
/// 搜索字幕 (ISubtitleProvider 接口实现)
|
||||
/// 根据媒体信息请求字幕列表。
|
||||
/// </summary>
|
||||
/// <param name="request">包含媒体路径、语言等信息的搜索请求对象</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>远程字幕信息列表</returns>
|
||||
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"{Name} Search | SubtitleSearchRequest -> { JsonSerializer.Serialize(request) }");
|
||||
|
||||
var subtitles = await SearchSubtitlesAsync(request);
|
||||
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询字幕
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
|
||||
{
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
try
|
||||
{
|
||||
var language = NormalizeLanguage(request.Language);
|
||||
|
||||
_logger.LogInformation("{Provider} Search | Target -> {File} | Language -> {Lang}", Name, Path.GetFileName(request.MediaPath), language);
|
||||
|
||||
if (language != "chi")
|
||||
{
|
||||
_logger.LogInformation("{Provider} Search | Summary -> Language not supported, skip search.", Name);
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
var cid = await GetCidByFileAsync(request.MediaPath);
|
||||
stopWatch.Stop();
|
||||
|
||||
_logger.LogInformation("{Provider} Search | FileHash -> {Hash} (Took {Elapsed}ms)", Name, cid, stopWatch.ElapsedMilliseconds);
|
||||
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri($"https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name={Path.GetFileName(request.MediaPath)}")
|
||||
};
|
||||
|
||||
using var httpClient = _httpClientFactory.CreateClient(Name);
|
||||
|
||||
var response = await httpClient.SendAsync(options);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var subtitleResponse = JsonSerializer.Deserialize<SubtitleResponseRoot>(await response.Content.ReadAsStringAsync(), _deserializeOptions);
|
||||
|
||||
if (subtitleResponse != null)
|
||||
{
|
||||
_logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitleResponse)}");
|
||||
|
||||
var subtitles = subtitleResponse.Data.Where(m => !string.IsNullOrEmpty(m.Name));
|
||||
|
||||
var remoteSubtitles = new List<RemoteSubtitleInfo>();
|
||||
|
||||
if (subtitles.Count() > 0)
|
||||
{
|
||||
foreach (var item in subtitles)
|
||||
{
|
||||
remoteSubtitles.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(JsonSerializer.Serialize(new DownloadSubInfo
|
||||
{
|
||||
Url = item.Url,
|
||||
Format = item.Ext,
|
||||
Language = request.Language,
|
||||
TwoLetterISOLanguageName = request.TwoLetterISOLanguageName,
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {item.Name} | {(item.Langs == string.Empty ? "未知" : item.Langs)} | 迅雷",
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = item.Ext,
|
||||
Comment = $"Format : {item.Ext}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
}); }
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get {subtitles.Count()} Subtitles");
|
||||
|
||||
return remoteSubtitles;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Provider} Search | Exception -> [{Type}] {Message}", Name, ex.GetType().Name, ex.Message);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles");
|
||||
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.MeiamSub.Thunder
|
||||
{
|
||||
/// <summary>
|
||||
/// 迅雷看看字幕提供程序
|
||||
/// 负责与迅雷 API 进行交互,通过 CID (Content ID) 匹配并下载字幕。
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// </summary>
|
||||
public class ThunderProvider : ISubtitleProvider, IHasOrder
|
||||
{
|
||||
#region 变量声明
|
||||
public const string ASS = "ass";
|
||||
public const string SSA = "ssa";
|
||||
public const string SRT = "srt";
|
||||
|
||||
private readonly ILogger<ThunderProvider> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private static readonly JsonSerializerOptions _deserializeOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
};
|
||||
|
||||
public int Order => 100;
|
||||
public string Name => "MeiamSub.Thunder";
|
||||
|
||||
/// <summary>
|
||||
/// 支持电影、剧集
|
||||
/// </summary>
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode };
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
public ThunderProvider(ILogger<ThunderProvider> logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger.LogInformation($"{Name} Init");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 查询字幕
|
||||
|
||||
/// <summary>
|
||||
/// 搜索字幕 (ISubtitleProvider 接口实现)
|
||||
/// 根据媒体信息请求字幕列表。
|
||||
/// </summary>
|
||||
/// <param name="request">包含媒体路径、语言等信息的搜索请求对象</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>远程字幕信息列表</returns>
|
||||
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"{Name} Search | SubtitleSearchRequest -> { JsonSerializer.Serialize(request) }");
|
||||
|
||||
var subtitles = await SearchSubtitlesAsync(request);
|
||||
|
||||
return subtitles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询字幕
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
|
||||
{
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
try
|
||||
{
|
||||
var language = NormalizeLanguage(request.Language);
|
||||
|
||||
_logger.LogInformation("{Provider} Search | Target -> {File} | Language -> {Lang}", Name, Path.GetFileName(request.MediaPath), language);
|
||||
|
||||
if (language != "chi")
|
||||
{
|
||||
_logger.LogInformation("{Provider} Search | Summary -> Language not supported, skip search.", Name);
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
var cid = await GetCidByFileAsync(request.MediaPath);
|
||||
stopWatch.Stop();
|
||||
|
||||
_logger.LogInformation("{Provider} Search | FileHash -> {Hash} (Took {Elapsed}ms)", Name, cid, stopWatch.ElapsedMilliseconds);
|
||||
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri($"https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name={Path.GetFileName(request.MediaPath)}")
|
||||
};
|
||||
|
||||
using var httpClient = _httpClientFactory.CreateClient(Name);
|
||||
|
||||
var response = await httpClient.SendAsync(options);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var subtitleResponse = JsonSerializer.Deserialize<SubtitleResponseRoot>(await response.Content.ReadAsStringAsync(), _deserializeOptions);
|
||||
|
||||
if (subtitleResponse != null)
|
||||
{
|
||||
_logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitleResponse)}");
|
||||
|
||||
var subtitles = subtitleResponse.Data.Where(m => !string.IsNullOrEmpty(m.Name));
|
||||
|
||||
var remoteSubtitles = new List<RemoteSubtitleInfo>();
|
||||
|
||||
if (subtitles.Count() > 0)
|
||||
{
|
||||
foreach (var item in subtitles)
|
||||
{
|
||||
remoteSubtitles.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(JsonSerializer.Serialize(new DownloadSubInfo
|
||||
{
|
||||
Url = item.Url,
|
||||
Format = item.Ext,
|
||||
Language = request.Language,
|
||||
TwoLetterISOLanguageName = request.TwoLetterISOLanguageName,
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {item.Name} | {(item.Langs == string.Empty ? "未知" : item.Langs)} | 迅雷",
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = item.Ext,
|
||||
Comment = $"Format : {item.Ext}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
}); }
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get {subtitles.Count()} Subtitles");
|
||||
|
||||
return remoteSubtitles;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{Provider} Search | Exception -> [{Type}] {Message}", Name, ex.GetType().Name, ex.Message);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles");
|
||||
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 下载字幕
|
||||
@@ -204,16 +204,17 @@ using MediaBrowser.Controller.Providers;
|
||||
|
||||
_logger.LogInformation($"{Name} DownloadSub | Url -> {downloadSub.Url} | Format -> {downloadSub.Format} | Language -> {downloadSub.Language} ");
|
||||
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(downloadSub.Url)
|
||||
};
|
||||
|
||||
using var httpClient = _httpClientFactory.CreateClient(Name);
|
||||
|
||||
var response = await httpClient.SendAsync(options);
|
||||
_logger.LogInformation($"{Name} DownloadSub | Response -> {response.StatusCode}");
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(downloadSub.Url)
|
||||
};
|
||||
|
||||
using var httpClient = _httpClientFactory.CreateClient(Name);
|
||||
|
||||
var response = await httpClient.SendAsync(options);
|
||||
|
||||
_logger.LogInformation($"{Name} DownloadSub | Response -> {response.StatusCode}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
@@ -230,7 +231,7 @@ using MediaBrowser.Controller.Providers;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{0} DownloadSub | Error -> {1}", Name, ex.Message);
|
||||
_logger.LogError(ex, "{Provider} DownloadSub | Exception -> [{Type}] {Message}", Name, ex.GetType().Name, ex.Message);
|
||||
}
|
||||
|
||||
return new SubtitleResponse();
|
||||
@@ -353,4 +354,4 @@ using MediaBrowser.Controller.Providers;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user