diff --git a/.gitignore b/.gitignore index 84f5aae..a0ce143 100644 --- a/.gitignore +++ b/.gitignore @@ -363,3 +363,6 @@ MigrationBackup/ FodyWeavers.xsd /GEMINI.md /TODO.md +/TestServer + +TestServer/ diff --git a/Emby.MeiamSub.Shooter/ShooterProvider.cs b/Emby.MeiamSub.Shooter/ShooterProvider.cs index b5dbd22..0ebe978 100644 --- a/Emby.MeiamSub.Shooter/ShooterProvider.cs +++ b/Emby.MeiamSub.Shooter/ShooterProvider.cs @@ -7,6 +7,7 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net; using System.Security.Cryptography; @@ -86,16 +87,21 @@ namespace Emby.MeiamSub.Shooter { var language = NormalizeLanguage(request.Language); + _logger.Info("{0} Search | Target -> {1} | Language -> {2}", Name, Path.GetFileName(request.MediaPath), language); + if (language != "chi" && language != "eng") { + _logger.Info("{0} Search | Summary -> Language not supported, skip search.", Name); return Array.Empty(); } FileInfo fileInfo = new FileInfo(request.MediaPath); + var stopWatch = Stopwatch.StartNew(); var hash = await ComputeFileHashAsync(fileInfo); + stopWatch.Stop(); - _logger.Info("{0} Search | FileHash -> {1}", new object[2] { Name, hash }); + _logger.Info("{0} Search | FileHash -> {1} (Took {2}ms)", new object[3] { Name, hash, stopWatch.ElapsedMilliseconds }); HttpRequestOptions options = new HttpRequestOptions { @@ -117,7 +123,7 @@ namespace Emby.MeiamSub.Shooter var response = await _httpClient.Post(options); - _logger.Debug("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) }); + _logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) }); if (response.StatusCode == HttpStatusCode.OK && response.ContentType.Contains("application/json")) { @@ -125,7 +131,7 @@ namespace Emby.MeiamSub.Shooter if (subtitleResponse != null) { - _logger.Debug("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) }); + _logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) }); var remoteSubtitles = new List(); @@ -160,7 +166,7 @@ namespace Emby.MeiamSub.Shooter } catch (Exception ex) { - _logger.Error("{0} Search | Error -> {1}", Name, ex.Message); + _logger.Error("{0} Search | Exception -> [{1}] {2}", Name, ex.GetType().Name, ex.Message); } _logger.Info("{0} Search | Summary -> Get 0 Subtitles", new object[1] { Name }); @@ -295,11 +301,15 @@ namespace Emby.MeiamSub.Shooter if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || - language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase)) + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh", StringComparison.OrdinalIgnoreCase) || + language.Equals("zho", StringComparison.OrdinalIgnoreCase) || + language.Equals("chi", StringComparison.OrdinalIgnoreCase)) { return "chi"; } - if (language.Equals("en", StringComparison.OrdinalIgnoreCase)) + if (language.Equals("en", StringComparison.OrdinalIgnoreCase) || + language.Equals("eng", StringComparison.OrdinalIgnoreCase)) { return "eng"; } diff --git a/Emby.MeiamSub.Thunder/ThunderProvider.cs b/Emby.MeiamSub.Thunder/ThunderProvider.cs index 9d3d0dc..9137352 100644 --- a/Emby.MeiamSub.Thunder/ThunderProvider.cs +++ b/Emby.MeiamSub.Thunder/ThunderProvider.cs @@ -1,4 +1,4 @@ -using Emby.MeiamSub.Thunder.Model; +using Emby.MeiamSub.Thunder.Model; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; @@ -7,6 +7,7 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -14,7 +15,6 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; -using static System.Net.WebRequestMethods; namespace Emby.MeiamSub.Thunder { @@ -88,14 +88,19 @@ namespace Emby.MeiamSub.Thunder { var language = NormalizeLanguage(request.Language); + _logger.Info("{0} Search | Target -> {1} | Language -> {2}", Name, Path.GetFileName(request.MediaPath), language); + if (language != "chi") { + _logger.Info("{0} Search | Summary -> Language not supported, skip search.", Name); return Array.Empty(); } + var stopWatch = Stopwatch.StartNew(); var cid = await GetCidByFileAsync(request.MediaPath); + stopWatch.Stop(); - _logger.Info("{0} Search | FileHash -> {1}", new object[2] { Name, cid }); + _logger.Info("{0} Search | FileHash -> {1} (Took {2}ms)", new object[3] { Name, cid, stopWatch.ElapsedMilliseconds }); HttpRequestOptions options = new HttpRequestOptions @@ -107,7 +112,7 @@ namespace Emby.MeiamSub.Thunder }; var response = await _httpClient.GetResponse(options); - _logger.Debug("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) }); + _logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) }); if (response.StatusCode == HttpStatusCode.OK) { @@ -115,7 +120,7 @@ namespace Emby.MeiamSub.Thunder if (subtitleResponse.Code == 0) { - _logger.Debug("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) }); + _logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) }); var subtitles = subtitleResponse.Data.Where(m => !string.IsNullOrEmpty(m.Name)); @@ -152,7 +157,7 @@ namespace Emby.MeiamSub.Thunder } catch (Exception ex) { - _logger.Error("{0} Search | Error -> {1}", Name, ex.Message); + _logger.Error("{0} Search | Exception -> [{1}] {2}", Name, ex.GetType().Name, ex.Message); } _logger.Info("{0} Search | Summary -> Get 0 Subtitles", new object[1] { Name }); @@ -286,12 +291,15 @@ namespace Emby.MeiamSub.Thunder if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || - language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase)) + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh", StringComparison.OrdinalIgnoreCase) || + language.Equals("zho", StringComparison.OrdinalIgnoreCase) || + language.Equals("chi", StringComparison.OrdinalIgnoreCase)) { return "chi"; } - // 迅雷可能只支持 chi,这里为了保持逻辑一致,也可以处理 eng,虽然 SearchSubtitlesAsync 会过滤掉 - if (language.Equals("en", StringComparison.OrdinalIgnoreCase)) + if (language.Equals("en", StringComparison.OrdinalIgnoreCase) || + language.Equals("eng", StringComparison.OrdinalIgnoreCase)) { return "eng"; } diff --git a/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs b/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs index 287fbf0..0d82359 100644 --- a/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs +++ b/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs @@ -7,6 +7,7 @@ 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; @@ -92,16 +93,21 @@ namespace Jellyfin.MeiamSub.Shooter { var language = NormalizeLanguage(request.Language); + _logger.LogInformation("{Provider} Search | Target -> {File} | Language -> {Lang}", Name, Path.GetFileName(request.MediaPath), language); + if (language != "chi" && language != "eng") { + _logger.LogInformation("{Provider} Search | Summary -> Language not supported, skip search.", Name); return Array.Empty(); } FileInfo fileInfo = new(request.MediaPath); + var stopWatch = Stopwatch.StartNew(); var hash = await ComputeFileHashAsync(fileInfo); + stopWatch.Stop(); - _logger.LogInformation($"{Name} Search | FileHash -> {hash}"); + _logger.LogInformation("{Provider} Search | FileHash -> {Hash} (Took {Elapsed}ms)", Name, hash, stopWatch.ElapsedMilliseconds); var formData = new Dictionary { @@ -121,18 +127,18 @@ namespace Jellyfin.MeiamSub.Shooter // 发送 POST 请求 var response = await httpClient.PostAsync(ApiUrl, content); - _logger.LogDebug($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}"); + _logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}"); // 处理响应 if (response.IsSuccessStatusCode && response.Content.Headers.Any(m => m.Value.Contains("application/json; charset=utf-8"))) { var responseBody = await response.Content.ReadAsStringAsync(); - _logger.LogDebug($"{Name} Search | ResponseBody -> {responseBody} "); + _logger.LogInformation($"{Name} Search | ResponseBody -> {responseBody} "); var subtitles = JsonSerializer.Deserialize>(responseBody); - _logger.LogDebug($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitles)}"); + _logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitles)}"); if (subtitles != null) { @@ -171,7 +177,7 @@ namespace Jellyfin.MeiamSub.Shooter } catch (Exception ex) { - _logger.LogError(ex, "{0} Search | Error -> {1}", Name, ex.Message); + _logger.LogError(ex, "{Provider} Search | Exception -> [{Type}] {Message}", Name, ex.GetType().Name, ex.Message); } _logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles"); @@ -307,11 +313,15 @@ namespace Jellyfin.MeiamSub.Shooter if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || - language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase)) + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh", StringComparison.OrdinalIgnoreCase) || + language.Equals("zho", StringComparison.OrdinalIgnoreCase) || + language.Equals("chi", StringComparison.OrdinalIgnoreCase)) { return "chi"; } - if (language.Equals("en", StringComparison.OrdinalIgnoreCase)) + if (language.Equals("en", StringComparison.OrdinalIgnoreCase) || + language.Equals("eng", StringComparison.OrdinalIgnoreCase)) { return "eng"; } diff --git a/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs b/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs index c3f0eb0..83d896f 100644 --- a/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs +++ b/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs @@ -1,250 +1,171 @@ 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.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 -{ - /// - /// 迅雷看看字幕提供程序 - /// 负责与迅雷 API 进行交互,通过 CID (Content ID) 匹配并下载字幕。 - /// 修改人: Meiam - /// 修改时间: 2025-12-22 - /// - public class ThunderProvider : ISubtitleProvider, IHasOrder - { - #region 变量声明 - public const string ASS = "ass"; - public const string SSA = "ssa"; - public const string SRT = "srt"; - - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private static readonly JsonSerializerOptions _deserializeOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }; - - public int Order => 1; - public string Name => "MeiamSub.Thunder"; - - /// - /// 支持电影、剧集 - /// - public IEnumerable SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode }; - #endregion - - #region 构造函数 - public ThunderProvider(ILogger logger, IHttpClientFactory httpClientFactory) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - _logger.LogInformation($"{Name} Init"); - } - #endregion - - #region 查询字幕 - - /// - /// 搜索字幕 (ISubtitleProvider 接口实现) - /// 根据媒体信息请求字幕列表。 - /// - /// 包含媒体路径、语言等信息的搜索请求对象 - /// 取消令牌 - /// 远程字幕信息列表 - public async Task> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) - { - _logger.LogInformation($"{Name} Search | SubtitleSearchRequest -> { JsonSerializer.Serialize(request) }"); - - var subtitles = await SearchSubtitlesAsync(request); - - return subtitles; - } - - /// - - /// 查询字幕 - - /// - - /// - - /// - - private async Task> SearchSubtitlesAsync(SubtitleSearchRequest request) - - { - - // 修改人: Meiam - - // 修改时间: 2025-12-22 - - // 备注: 增加异常处理 - - - - try - - { - - var language = NormalizeLanguage(request.Language); - - - - if (language != "chi") - - { - - return Array.Empty(); - - } - - - - var cid = await GetCidByFileAsync(request.MediaPath); - - - - _logger.LogInformation($"{Name} Search | FileHash -> {cid}"); - - - - 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.LogDebug($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}"); - - - - if (response.StatusCode == HttpStatusCode.OK) - - { - - var subtitleResponse = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(), _deserializeOptions); - - - - if (subtitleResponse != null) - - { - - _logger.LogDebug($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitleResponse)}"); - - - - var subtitles = subtitleResponse.Data.Where(m => !string.IsNullOrEmpty(m.Name)); - - - - var remoteSubtitles = new List(); - - - - 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 = 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, "{0} Search | Error -> {1}", Name, ex.Message); - - } - - - - _logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles"); - - - - return Array.Empty(); - - } +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 +{ + /// + /// 迅雷看看字幕提供程序 + /// 负责与迅雷 API 进行交互,通过 CID (Content ID) 匹配并下载字幕。 + /// 修改人: Meiam + /// 修改时间: 2025-12-22 + /// + public class ThunderProvider : ISubtitleProvider, IHasOrder + { + #region 变量声明 + public const string ASS = "ass"; + public const string SSA = "ssa"; + public const string SRT = "srt"; + + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private static readonly JsonSerializerOptions _deserializeOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + + public int Order => 1; + public string Name => "MeiamSub.Thunder"; + + /// + /// 支持电影、剧集 + /// + public IEnumerable SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode }; + #endregion + + #region 构造函数 + public ThunderProvider(ILogger logger, IHttpClientFactory httpClientFactory) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _logger.LogInformation($"{Name} Init"); + } + #endregion + + #region 查询字幕 + + /// + /// 搜索字幕 (ISubtitleProvider 接口实现) + /// 根据媒体信息请求字幕列表。 + /// + /// 包含媒体路径、语言等信息的搜索请求对象 + /// 取消令牌 + /// 远程字幕信息列表 + public async Task> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) + { + _logger.LogInformation($"{Name} Search | SubtitleSearchRequest -> { JsonSerializer.Serialize(request) }"); + + var subtitles = await SearchSubtitlesAsync(request); + + return subtitles; + } + + /// + /// 查询字幕 + /// + /// + /// + private async Task> 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(); + } + + 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(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(); + + 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 = 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(); + } #endregion #region 下载字幕 @@ -372,11 +293,15 @@ namespace Jellyfin.MeiamSub.Thunder if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || - language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase)) + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh", StringComparison.OrdinalIgnoreCase) || + language.Equals("zho", StringComparison.OrdinalIgnoreCase) || + language.Equals("chi", StringComparison.OrdinalIgnoreCase)) { return "chi"; } - if (language.Equals("en", StringComparison.OrdinalIgnoreCase)) + if (language.Equals("en", StringComparison.OrdinalIgnoreCase) || + language.Equals("eng", StringComparison.OrdinalIgnoreCase)) { return "eng"; }