refactor: 深度架构优化与 Jellyfin/Emby 规范化对齐

1. 引入 IHttpClientFactory:重构 Jellyfin 插件的 HTTP 调用模式,使用依赖注入管理 HttpClient 生命周期。
2. 优化服务注册:在 PluginServiceRegistrator 中统一配置命名 HTTP 客户端的超时与默认 Header。
3. 增强资源加载:优化 Emby 插件缩略图获取逻辑,增加安全性检查。
4. 代码逻辑优化:提取私有方法 NormalizeLanguage 统一处理跨平台语言代码映射。
5. 精简配置:简化 PluginConfiguration,移除冗余引用,同时确保与 Jellyfin 10.x 基类兼容。
6. 日志分级:细化 API 响应内容的记录级别,减少生产环境日志输出。
This commit is contained in:
Meiam
2025-12-22 14:24:05 +08:00
parent 6743851405
commit f48fc0910b
10 changed files with 269 additions and 123 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,12 +1,8 @@
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Plugins;
namespace Jellyfin.MeiamSub.Shooter.Configuration
{
/// <summary>
/// Plugin configuration.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
}
}

View File

@@ -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;
}

View File

@@ -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<ISubtitleProvider, ShooterProvider>();
}
}

View File

@@ -36,8 +36,7 @@ namespace Jellyfin.MeiamSub.Shooter
public const string SRT = "srt";
private readonly ILogger<ShooterProvider> _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<ShooterProvider> logger)
public ShooterProvider(ILogger<ShooterProvider> 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<RemoteSubtitleInfo>();
}
@@ -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<List<SubtitleResponseRoot>>(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;
}
/// <summary>
/// 规范化语言代码
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 异步计算文件 Hash (射手网专用算法)
/// <para>修改人: Meiam</para>

View File

@@ -1,12 +1,8 @@
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Plugins;
namespace Jellyfin.MeiamSub.Thunder.Configuration
{
/// <summary>
/// Plugin configuration.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
}
}

View File

@@ -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;

View File

@@ -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<ISubtitleProvider, ThunderProvider>();
}
}

View File

@@ -32,7 +32,7 @@ namespace Jellyfin.MeiamSub.Thunder
public const string SRT = "srt";
private readonly ILogger<ThunderProvider> _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<ThunderProvider> logger)
public ThunderProvider(ILogger<ThunderProvider> 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;
}
/// <summary>
/// 查询字幕
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
{
// 修改人: Meiam
// 修改时间: 2025-12-22
// 备注: 增加异常处理
/// <summary>
/// 查询字幕
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
try
{
if (request.Language != "chi")
{
return Array.Empty<RemoteSubtitleInfo>();
}
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<SubtitleResponseRoot>(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<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,
});
}
return Array.Empty<RemoteSubtitleInfo>();
}
_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<SubtitleResponseRoot>(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<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 = 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<RemoteSubtitleInfo>();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "{0} Search | Error -> {1}", Name, ex.Message);
}
_logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles");
return Array.Empty<RemoteSubtitleInfo>();
}
#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;
}
/// <summary>
/// 规范化语言代码
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 异步计算文件 CID (迅雷专用算法)
/// <para>修改人: Meiam</para>