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