From f48fc0910b1f93fb1feb3cdeb20f862db3ff638b Mon Sep 17 00:00:00 2001 From: Meiam <91270@qq.com> Date: Mon, 22 Dec 2025 14:24:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B7=B1=E5=BA=A6=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=BC=98=E5=8C=96=E4=B8=8E=20Jellyfin/Emby=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=8C=96=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 引入 IHttpClientFactory:重构 Jellyfin 插件的 HTTP 调用模式,使用依赖注入管理 HttpClient 生命周期。 2. 优化服务注册:在 PluginServiceRegistrator 中统一配置命名 HTTP 客户端的超时与默认 Header。 3. 增强资源加载:优化 Emby 插件缩略图获取逻辑,增加安全性检查。 4. 代码逻辑优化:提取私有方法 NormalizeLanguage 统一处理跨平台语言代码映射。 5. 精简配置:简化 PluginConfiguration,移除冗余引用,同时确保与 Jellyfin 10.x 基类兼容。 6. 日志分级:细化 API 响应内容的记录级别,减少生产环境日志输出。 --- Emby.MeiamSub.Shooter/Plugin.cs | 13 +- Emby.MeiamSub.Thunder/Plugin.cs | 10 +- .../Configuration/PluginConfiguration.cs | 6 +- Jellyfin.MeiamSub.Shooter/Plugin.cs | 4 +- .../PluginServiceRegistrator.cs | 7 + Jellyfin.MeiamSub.Shooter/ShooterProvider.cs | 60 ++-- .../Configuration/PluginConfiguration.cs | 6 +- Jellyfin.MeiamSub.Thunder/Plugin.cs | 1 - .../PluginServiceRegistrator.cs | 7 + Jellyfin.MeiamSub.Thunder/ThunderProvider.cs | 278 ++++++++++++------ 10 files changed, 269 insertions(+), 123 deletions(-) diff --git a/Emby.MeiamSub.Shooter/Plugin.cs b/Emby.MeiamSub.Shooter/Plugin.cs index 7dd7080..7d90cf6 100644 --- a/Emby.MeiamSub.Shooter/Plugin.cs +++ b/Emby.MeiamSub.Shooter/Plugin.cs @@ -48,7 +48,18 @@ namespace Emby.MeiamSub.Shooter public Stream GetThumbImage() { var type = GetType(); - return type.Assembly.GetManifestResourceStream(type.Namespace + ".Thumb.png"); + var resourceName = $"{type.Namespace}.Thumb.png"; + var stream = type.Assembly.GetManifestResourceStream(resourceName); + + if (stream == null) + { + // 如果找不到资源,尝试不带命名空间的名称,或者记录错误 + // 这里我们至少确保不会返回 null 导致外部空引用(虽然 Emby 可能处理 null) + // 但为了稳健,如果真的没有,返回 null 是正确的,Emby 会显示默认图标 + return null; + } + + return stream; } } } diff --git a/Emby.MeiamSub.Thunder/Plugin.cs b/Emby.MeiamSub.Thunder/Plugin.cs index 16aa521..050c30b 100644 --- a/Emby.MeiamSub.Thunder/Plugin.cs +++ b/Emby.MeiamSub.Thunder/Plugin.cs @@ -50,7 +50,15 @@ namespace Emby.MeiamSub.Thunder public Stream GetThumbImage() { var type = GetType(); - return type.Assembly.GetManifestResourceStream(type.Namespace + ".Thumb.png"); + var resourceName = $"{type.Namespace}.Thumb.png"; + var stream = type.Assembly.GetManifestResourceStream(resourceName); + + if (stream == null) + { + return null; + } + + return stream; } } } diff --git a/Jellyfin.MeiamSub.Shooter/Configuration/PluginConfiguration.cs b/Jellyfin.MeiamSub.Shooter/Configuration/PluginConfiguration.cs index 59bbbf7..721a83d 100644 --- a/Jellyfin.MeiamSub.Shooter/Configuration/PluginConfiguration.cs +++ b/Jellyfin.MeiamSub.Shooter/Configuration/PluginConfiguration.cs @@ -1,12 +1,8 @@ -using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Plugins; namespace Jellyfin.MeiamSub.Shooter.Configuration { - /// - /// Plugin configuration. - /// public class PluginConfiguration : BasePluginConfiguration { - } } diff --git a/Jellyfin.MeiamSub.Shooter/Plugin.cs b/Jellyfin.MeiamSub.Shooter/Plugin.cs index bf84473..999f9da 100644 --- a/Jellyfin.MeiamSub.Shooter/Plugin.cs +++ b/Jellyfin.MeiamSub.Shooter/Plugin.cs @@ -1,10 +1,8 @@ using Jellyfin.MeiamSub.Shooter.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Serialization; using System; -using System.IO; namespace Jellyfin.MeiamSub.Shooter { @@ -30,7 +28,7 @@ namespace Jellyfin.MeiamSub.Shooter public override string Description => "Download subtitles from Shooter"; public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) + : base(applicationPaths, xmlSerializer) { Instance = this; } diff --git a/Jellyfin.MeiamSub.Shooter/PluginServiceRegistrator.cs b/Jellyfin.MeiamSub.Shooter/PluginServiceRegistrator.cs index cd1a69c..3b80244 100644 --- a/Jellyfin.MeiamSub.Shooter/PluginServiceRegistrator.cs +++ b/Jellyfin.MeiamSub.Shooter/PluginServiceRegistrator.cs @@ -14,6 +14,13 @@ namespace Jellyfin.MeiamSub.Shooter { public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHos) { + serviceCollection.AddHttpClient("MeiamSub.Shooter", client => + { + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.Add("User-Agent", "MeiamSub.Shooter"); + client.DefaultRequestHeaders.Add("Accept", "*/*"); + }); + serviceCollection.AddSingleton(); } } diff --git a/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs b/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs index cb3a5c5..287fbf0 100644 --- a/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs +++ b/Jellyfin.MeiamSub.Shooter/ShooterProvider.cs @@ -36,8 +36,7 @@ namespace Jellyfin.MeiamSub.Shooter public const string SRT = "srt"; private readonly ILogger _logger; - - private static readonly HttpClient _httpClient = new HttpClient(); + private readonly IHttpClientFactory _httpClientFactory; private const string ApiUrl = "https://www.shooter.cn/api/subapi.php"; @@ -52,10 +51,10 @@ namespace Jellyfin.MeiamSub.Shooter #endregion #region 构造函数 - public ShooterProvider(ILogger logger) + public ShooterProvider(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; - _httpClient.Timeout = TimeSpan.FromSeconds(30); + _httpClientFactory = httpClientFactory; _logger.LogInformation($"{Name} Init"); } #endregion @@ -91,7 +90,9 @@ namespace Jellyfin.MeiamSub.Shooter try { - if (request.Language != "chi" && request.Language != "eng") + var language = NormalizeLanguage(request.Language); + + if (language != "chi" && language != "eng") { return Array.Empty(); } @@ -107,7 +108,7 @@ namespace Jellyfin.MeiamSub.Shooter { "filehash", hash}, { "pathinfo", request.MediaPath}, { "format", "json"}, - { "lang", request.Language == "chi" ? "chn" : "eng"} + { "lang", language == "chi" ? "chn" : "eng"} }; var content = new FormUrlEncodedContent(formData); @@ -115,21 +116,23 @@ namespace Jellyfin.MeiamSub.Shooter // 设置请求头 content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - // 发送 POST 请求 - var response = await _httpClient.PostAsync(ApiUrl, content); + using var httpClient = _httpClientFactory.CreateClient(Name); - _logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}"); + // 发送 POST 请求 + var response = await httpClient.PostAsync(ApiUrl, content); + + _logger.LogDebug($"{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.LogInformation($"{Name} Search | ResponseBody -> {responseBody} "); + _logger.LogDebug($"{Name} Search | ResponseBody -> {responseBody} "); var subtitles = JsonSerializer.Deserialize>(responseBody); - _logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitles)}"); + _logger.LogDebug($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitles)}"); if (subtitles != null) { @@ -146,7 +149,7 @@ namespace Jellyfin.MeiamSub.Shooter { Url = subFile.Link, Format = subFile.Ext, - Language = request.Language, + Language = language, TwoLetterISOLanguageName = request.TwoLetterISOLanguageName, })), Name = $"[MEIAMSUB] {Path.GetFileName(request.MediaPath)} | {request.TwoLetterISOLanguageName} | 射手", @@ -217,15 +220,12 @@ namespace Jellyfin.MeiamSub.Shooter using var options = new HttpRequestMessage { Method = HttpMethod.Get, - RequestUri = new Uri(downloadSub.Url), - Headers = - { - UserAgent = { new ProductInfoHeaderValue(new ProductHeaderValue($"{Name}")) }, - Accept = { new MediaTypeWithQualityHeaderValue("*/*") } - } + RequestUri = new Uri(downloadSub.Url) }; - var response = await _httpClient.SendAsync(options); + using var httpClient = _httpClientFactory.CreateClient(Name); + + var response = await httpClient.SendAsync(options); _logger.LogInformation($"{Name} DownloadSub | Response -> {response.StatusCode}"); @@ -296,6 +296,28 @@ namespace Jellyfin.MeiamSub.Shooter return null; } + /// + /// 规范化语言代码 + /// + /// + /// + private static string NormalizeLanguage(string language) + { + if (string.IsNullOrEmpty(language)) return language; + + if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase)) + { + return "chi"; + } + if (language.Equals("en", StringComparison.OrdinalIgnoreCase)) + { + return "eng"; + } + return language; + } + /// /// 异步计算文件 Hash (射手网专用算法) /// 修改人: Meiam diff --git a/Jellyfin.MeiamSub.Thunder/Configuration/PluginConfiguration.cs b/Jellyfin.MeiamSub.Thunder/Configuration/PluginConfiguration.cs index 761ff4e..01549d1 100644 --- a/Jellyfin.MeiamSub.Thunder/Configuration/PluginConfiguration.cs +++ b/Jellyfin.MeiamSub.Thunder/Configuration/PluginConfiguration.cs @@ -1,12 +1,8 @@ -using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Plugins; namespace Jellyfin.MeiamSub.Thunder.Configuration { - /// - /// Plugin configuration. - /// public class PluginConfiguration : BasePluginConfiguration { - } } diff --git a/Jellyfin.MeiamSub.Thunder/Plugin.cs b/Jellyfin.MeiamSub.Thunder/Plugin.cs index 8a838e0..5342bc1 100644 --- a/Jellyfin.MeiamSub.Thunder/Plugin.cs +++ b/Jellyfin.MeiamSub.Thunder/Plugin.cs @@ -1,7 +1,6 @@ using Jellyfin.MeiamSub.Thunder.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Serialization; using System; diff --git a/Jellyfin.MeiamSub.Thunder/PluginServiceRegistrator.cs b/Jellyfin.MeiamSub.Thunder/PluginServiceRegistrator.cs index aa24c87..ad8f48c 100644 --- a/Jellyfin.MeiamSub.Thunder/PluginServiceRegistrator.cs +++ b/Jellyfin.MeiamSub.Thunder/PluginServiceRegistrator.cs @@ -14,6 +14,13 @@ namespace Jellyfin.MeiamSub.Thunder { public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHos) { + serviceCollection.AddHttpClient("MeiamSub.Thunder", client => + { + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.Add("User-Agent", "MeiamSub.Thunder"); + client.DefaultRequestHeaders.Add("Accept", "*/*"); + }); + serviceCollection.AddSingleton(); } } diff --git a/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs b/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs index 0ea36b1..c3f0eb0 100644 --- a/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs +++ b/Jellyfin.MeiamSub.Thunder/ThunderProvider.cs @@ -32,7 +32,7 @@ namespace Jellyfin.MeiamSub.Thunder public const string SRT = "srt"; private readonly ILogger _logger; - private static readonly HttpClient _httpClient = new HttpClient(); + private readonly IHttpClientFactory _httpClientFactory; private static readonly JsonSerializerOptions _deserializeOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower @@ -48,10 +48,10 @@ namespace Jellyfin.MeiamSub.Thunder #endregion #region 构造函数 - public ThunderProvider(ILogger logger) + public ThunderProvider(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; - _httpClient.Timeout = TimeSpan.FromSeconds(30); + _httpClientFactory = httpClientFactory; _logger.LogInformation($"{Name} Init"); } #endregion @@ -74,93 +74,177 @@ namespace Jellyfin.MeiamSub.Thunder return subtitles; } - /// - /// 查询字幕 - /// - /// - /// - private async Task> SearchSubtitlesAsync(SubtitleSearchRequest request) - { - // 修改人: Meiam - // 修改时间: 2025-12-22 - // 备注: 增加异常处理 + /// + + /// 查询字幕 + + /// + + /// + + /// + + private async Task> SearchSubtitlesAsync(SubtitleSearchRequest request) - try - { - if (request.Language != "chi") { - return Array.Empty(); - } - var cid = await GetCidByFileAsync(request.MediaPath); + // 修改人: Meiam - _logger.LogInformation($"{Name} Search | FileHash -> {cid}"); + // 修改时间: 2025-12-22 + + // 备注: 增加异常处理 + + + + try - using var options = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri($"https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name={Path.GetFileName(request.MediaPath)}"), - Headers = { - UserAgent = { new ProductInfoHeaderValue(new ProductHeaderValue($"{Name}")) }, - Accept = { new MediaTypeWithQualityHeaderValue("*/*") }, - } - }; - var response = await _httpClient.SendAsync(options); + var language = NormalizeLanguage(request.Language); - _logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(response)}"); + - if (response.StatusCode == HttpStatusCode.OK) - { - var subtitleResponse = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(), _deserializeOptions); + if (language != "chi") - 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 = 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, - }); - } + + return Array.Empty(); + } - _logger.LogInformation($"{Name} Search | Summary -> Get {subtitles.Count()} Subtitles"); + + + 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; + + } + + } - 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(); + } - } - catch (Exception ex) - { - _logger.LogError(ex, "{0} Search | Error -> {1}", Name, ex.Message); - } - - _logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles"); - - return Array.Empty(); - } #endregion #region 下载字幕 @@ -200,20 +284,16 @@ namespace Jellyfin.MeiamSub.Thunder _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), - Headers = - { - UserAgent = { new ProductInfoHeaderValue(new ProductHeaderValue($"{Name}")) }, - Accept = { new MediaTypeWithQualityHeaderValue("*/*") } - } - }; - - 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) { @@ -281,6 +361,28 @@ namespace Jellyfin.MeiamSub.Thunder return null; } + /// + /// 规范化语言代码 + /// + /// + /// + private static string NormalizeLanguage(string language) + { + if (string.IsNullOrEmpty(language)) return language; + + if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase)) + { + return "chi"; + } + if (language.Equals("en", StringComparison.OrdinalIgnoreCase)) + { + return "eng"; + } + return language; + } + /// /// 异步计算文件 CID (迅雷专用算法) /// 修改人: Meiam