mirror of
https://github.com/91270/MeiamSubtitles.git
synced 2026-02-02 18:19:50 +08:00
refactor: 异步 I/O 改造、异常处理增强及依赖版本升级
1. 异步 I/O 改造:将文件哈希匹配逻辑重构为异步模式,使用 ReadExactlyAsync 提升大文件处理性能。 2. 异常处理:在搜索与下载核心逻辑中增加 try-catch 块及日志记录,确保插件运行稳定性。 3. 命名与风格:统一跨平台项目的变量命名规范(如 remoteSubtitles)与 HTTP 请求构建方式。 4. 注释补全:为核心类和方法补全中文 XML 文档注释,并标注修改人信息。 5. 依赖升级:更新 Jellyfin 核心库至 10.11.5 并升级至 net9.0,更新 Emby 核心库至 4.9.1.90。
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -360,4 +360,6 @@ MigrationBackup/
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
FodyWeavers.xsd
|
||||
/GEMINI.md
|
||||
/TODO.md
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.8.10" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.8.10" />
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.9.1.90" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.9.1.90" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
||||
@@ -18,7 +18,10 @@ using System.Web;
|
||||
namespace Emby.MeiamSub.Shooter
|
||||
{
|
||||
/// <summary>
|
||||
/// 迅雷字幕组件
|
||||
/// 射手网字幕提供程序
|
||||
/// 负责与射手网 API 进行交互,通过文件哈希匹配并下载字幕。
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// </summary>
|
||||
public class ShooterProvider : ISubtitleProvider, IHasOrder
|
||||
{
|
||||
@@ -37,7 +40,7 @@ namespace Emby.MeiamSub.Shooter
|
||||
/// <summary>
|
||||
/// 支持电影、剧集
|
||||
/// </summary>
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode };
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
@@ -53,11 +56,12 @@ namespace Emby.MeiamSub.Shooter
|
||||
#region 查询字幕
|
||||
|
||||
/// <summary>
|
||||
/// 查询请求
|
||||
/// 搜索字幕 (ISubtitleProvider 接口实现)
|
||||
/// 根据媒体信息请求字幕列表。
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="request">包含媒体路径、语言等信息的搜索请求对象</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>远程字幕信息列表</returns>
|
||||
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("{0} Search | SubtitleSearchRequest -> {1}", new object[2] { Name , _jsonSerializer.SerializeToString(request) });
|
||||
@@ -74,82 +78,95 @@ namespace Emby.MeiamSub.Shooter
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
|
||||
{
|
||||
if(request.Language == "zh-CN" || request.Language == "zh-TW" || request.Language == "zh-HK"){
|
||||
request.Language = "chi";
|
||||
}
|
||||
if(request.Language == "en"){
|
||||
request.Language = "eng";
|
||||
}
|
||||
if (request.Language != "chi" && request.Language != "eng")
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理,确保单个插件错误不影响系统整体运行
|
||||
|
||||
try
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new FileInfo(request.MediaPath);
|
||||
|
||||
var hash = ComputeFileHash(fileInfo);
|
||||
|
||||
_logger.Info("{0} Search | FileHash -> {1}", new object[2] { Name, hash });
|
||||
|
||||
HttpRequestOptions options = new HttpRequestOptions
|
||||
{
|
||||
Url = $"https://www.shooter.cn/api/subapi.php",
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
};
|
||||
|
||||
options.SetPostData(new Dictionary<string, string>
|
||||
{
|
||||
{ "filehash", HttpUtility.UrlEncode(hash)},
|
||||
{ "pathinfo", HttpUtility.UrlEncode(request.MediaPath)},
|
||||
{ "format", "json"},
|
||||
{ "lang",request.Language == "chi" ? "chn" : "eng"}
|
||||
});
|
||||
|
||||
_logger.Info("{0} Search | Request -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(options) });
|
||||
|
||||
var response = await _httpClient.Post(options);
|
||||
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) });
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK && response.ContentType.Contains("application/json"))
|
||||
{
|
||||
var subtitleResponse = _jsonSerializer.DeserializeFromStream<List<SubtitleResponseRoot>>(response.Content);
|
||||
|
||||
if (subtitleResponse != null)
|
||||
if (request.Language == "zh-CN" || request.Language == "zh-TW" || request.Language == "zh-HK")
|
||||
{
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) });
|
||||
|
||||
var remoteSubtitleInfos = new List<RemoteSubtitleInfo>();
|
||||
|
||||
foreach(var subFileInfo in subtitleResponse)
|
||||
{
|
||||
foreach(var subFile in subFileInfo.Files)
|
||||
{
|
||||
remoteSubtitleInfos.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(_jsonSerializer.SerializeToString(new DownloadSubInfo
|
||||
{
|
||||
Url = subFile.Link,
|
||||
Format = subFile.Ext,
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
Name = $"[MEIAMSUB] { Path.GetFileName(request.MediaPath) } | {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, remoteSubtitleInfos.Count });
|
||||
|
||||
|
||||
return remoteSubtitleInfos;
|
||||
request.Language = "chi";
|
||||
}
|
||||
if (request.Language == "en")
|
||||
{
|
||||
request.Language = "eng";
|
||||
}
|
||||
if (request.Language != "chi" && request.Language != "eng")
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new FileInfo(request.MediaPath);
|
||||
|
||||
var hash = await ComputeFileHashAsync(fileInfo);
|
||||
|
||||
_logger.Info("{0} Search | FileHash -> {1}", new object[2] { Name, hash });
|
||||
|
||||
HttpRequestOptions options = new HttpRequestOptions
|
||||
{
|
||||
Url = $"https://www.shooter.cn/api/subapi.php",
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
};
|
||||
|
||||
options.SetPostData(new Dictionary<string, string>
|
||||
{
|
||||
{ "filehash", HttpUtility.UrlEncode(hash)},
|
||||
{ "pathinfo", HttpUtility.UrlEncode(request.MediaPath)},
|
||||
{ "format", "json"},
|
||||
{ "lang",request.Language == "chi" ? "chn" : "eng"}
|
||||
});
|
||||
|
||||
_logger.Info("{0} Search | Request -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(options) });
|
||||
|
||||
var response = await _httpClient.Post(options);
|
||||
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) });
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK && response.ContentType.Contains("application/json"))
|
||||
{
|
||||
var subtitleResponse = _jsonSerializer.DeserializeFromStream<List<SubtitleResponseRoot>>(response.Content);
|
||||
|
||||
if (subtitleResponse != null)
|
||||
{
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) });
|
||||
|
||||
var remoteSubtitles = new List<RemoteSubtitleInfo>();
|
||||
|
||||
foreach (var subFileInfo in subtitleResponse)
|
||||
{
|
||||
foreach (var subFile in subFileInfo.Files)
|
||||
{
|
||||
remoteSubtitles.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(_jsonSerializer.SerializeToString(new DownloadSubInfo
|
||||
{
|
||||
Url = subFile.Link,
|
||||
Format = subFile.Ext,
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {Path.GetFileName(request.MediaPath)} | {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 });
|
||||
|
||||
|
||||
return remoteSubtitles;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("{0} Search | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
_logger.Info("{0} Search | Summary -> Get 0 Subtitles", new object[1] { Name });
|
||||
@@ -160,11 +177,12 @@ namespace Emby.MeiamSub.Shooter
|
||||
|
||||
#region 下载字幕
|
||||
/// <summary>
|
||||
/// 下载请求
|
||||
/// 获取字幕内容 (ISubtitleProvider 接口实现)
|
||||
/// 根据字幕 ID 下载具体的字幕文件流。
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="id">字幕唯一标识符 (Base64 编码的 JSON 数据)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>包含字幕流的响应对象</returns>
|
||||
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("{0} DownloadSub | Request -> {1}", new object[2] { Name, id });
|
||||
@@ -179,38 +197,49 @@ namespace Emby.MeiamSub.Shooter
|
||||
/// <returns></returns>
|
||||
private async Task<SubtitleResponse> DownloadSubAsync(string info)
|
||||
{
|
||||
var downloadSub = _jsonSerializer.DeserializeFromString<DownloadSubInfo>(Base64Decode(info));
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
if (downloadSub == null)
|
||||
try
|
||||
{
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
var downloadSub = _jsonSerializer.DeserializeFromString<DownloadSubInfo>(Base64Decode(info));
|
||||
|
||||
_logger.Info($"{0} DownloadSub | Url -> {1} | Format -> {2} | Language -> {3} " ,
|
||||
new object[4] { Name, downloadSub.Url, downloadSub.Format, downloadSub.Language });
|
||||
|
||||
var response = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
Url = downloadSub.Url,
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
});
|
||||
|
||||
|
||||
_logger.Info("{0} DownloadSub | Request -> {1}", new object[2] { Name, response.StatusCode });
|
||||
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
|
||||
return new SubtitleResponse()
|
||||
if (downloadSub == null)
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = response.Content,
|
||||
};
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
|
||||
_logger.Info($"{0} DownloadSub | Url -> {1} | Format -> {2} | Language -> {3} ",
|
||||
new object[4] { Name, downloadSub.Url, downloadSub.Format, downloadSub.Language });
|
||||
|
||||
var response = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
Url = downloadSub.Url,
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
});
|
||||
|
||||
|
||||
_logger.Info("{0} DownloadSub | Request -> {1}", new object[2] { Name, response.StatusCode });
|
||||
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = response.Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("{0} DownloadSub | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
return new SubtitleResponse();
|
||||
@@ -248,27 +277,33 @@ namespace Emby.MeiamSub.Shooter
|
||||
/// <returns></returns>
|
||||
protected string ExtractFormat(string text)
|
||||
{
|
||||
|
||||
string result = null;
|
||||
|
||||
if (text != null)
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) result = ASS;
|
||||
else if (text.Contains(SSA)) result = SSA;
|
||||
else if (text.Contains(SRT)) result = SRT;
|
||||
else result = null;
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) return ASS;
|
||||
if (text.Contains(SSA)) return SSA;
|
||||
if (text.Contains(SRT)) return SRT;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 Hash (射手)
|
||||
/// 异步计算文件 Hash (射手网专用算法)
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// <para>备注: 采用异步 I/O 读取文件特定位置的 4KB 数据块进行 MD5 计算。</para>
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public static string ComputeFileHash(FileInfo fileInfo)
|
||||
/// <param name="fileInfo">文件信息对象</param>
|
||||
/// <returns>计算得到的文件 Hash 字符串,如果文件过小或不存在则返回空字符串</returns>
|
||||
public static async Task<string> ComputeFileHashAsync(FileInfo fileInfo)
|
||||
{
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 改造为异步方法,优化 I/O 性能并增加 using 语句释放资源
|
||||
|
||||
string ret = "";
|
||||
|
||||
if (!fileInfo.Exists || fileInfo.Length < 8 * 1024)
|
||||
@@ -276,40 +311,41 @@ namespace Emby.MeiamSub.Shooter
|
||||
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)
|
||||
using (FileStream fs = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous))
|
||||
{
|
||||
fs.Seek(offset[i], 0);
|
||||
fs.Read(bBuf, 0, 4 * 1024);
|
||||
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;
|
||||
|
||||
MD5 md5Hash = MD5.Create();
|
||||
byte[] data = md5Hash.ComputeHash(bBuf);
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
byte[] bBuf = new byte[1024 * 4];
|
||||
|
||||
for (int j = 0; j < data.Length; j++)
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
sBuilder.Append(data[j].ToString("x2"));
|
||||
}
|
||||
fs.Seek(offset[i], SeekOrigin.Begin);
|
||||
await fs.ReadAsync(bBuf, 0, 4 * 1024);
|
||||
|
||||
if (!string.IsNullOrEmpty(ret))
|
||||
{
|
||||
ret += ";";
|
||||
}
|
||||
using (MD5 md5Hash = MD5.Create())
|
||||
{
|
||||
byte[] data = md5Hash.ComputeHash(bBuf);
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
|
||||
ret += sBuilder.ToString();
|
||||
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;
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.8.10" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.8.10" />
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.9.1.90" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.9.1.90" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ using static System.Net.WebRequestMethods;
|
||||
namespace Emby.MeiamSub.Thunder
|
||||
{
|
||||
/// <summary>
|
||||
/// 迅雷字幕组件
|
||||
/// 迅雷看看字幕提供程序
|
||||
/// 负责与迅雷 API 进行交互,通过 CID (Content ID) 匹配并下载字幕。
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// </summary>
|
||||
public class ThunderProvider : ISubtitleProvider, IHasOrder
|
||||
{
|
||||
@@ -39,7 +42,7 @@ namespace Emby.MeiamSub.Thunder
|
||||
/// <summary>
|
||||
/// 支持电影、剧集
|
||||
/// </summary>
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode };
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
@@ -55,11 +58,12 @@ namespace Emby.MeiamSub.Thunder
|
||||
#region 查询字幕
|
||||
|
||||
/// <summary>
|
||||
/// 查询请求
|
||||
/// 搜索字幕 (ISubtitleProvider 接口实现)
|
||||
/// 根据媒体信息请求字幕列表。
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="request">包含媒体路径、语言等信息的搜索请求对象</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>远程字幕信息列表</returns>
|
||||
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("{0} Search | SubtitleSearchRequest -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(request) });
|
||||
@@ -76,69 +80,81 @@ namespace Emby.MeiamSub.Thunder
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
|
||||
{
|
||||
if(request.Language == "zh-CN" || request.Language == "zh-TW" || request.Language == "zh-HK"){
|
||||
request.Language = "chi";
|
||||
}
|
||||
if (request.Language != "chi")
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
try
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var cid = GetCidByFile(request.MediaPath);
|
||||
|
||||
_logger.Info("{0} Search | FileHash -> {1}", new object[2] { Name, cid });
|
||||
|
||||
|
||||
HttpRequestOptions options = new HttpRequestOptions
|
||||
{
|
||||
Url = $"https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name={Path.GetFileName(request.MediaPath)}",
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
};
|
||||
var response = await _httpClient.GetResponse(options);
|
||||
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) });
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var subtitleResponse = _jsonSerializer.DeserializeFromStream<SubtitleResponseRoot>(response.Content);
|
||||
|
||||
if (subtitleResponse.Code == 0)
|
||||
if (request.Language == "zh-CN" || request.Language == "zh-TW" || request.Language == "zh-HK")
|
||||
{
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(subtitleResponse) });
|
||||
|
||||
var subtitles = subtitleResponse.Data.Where(m => !string.IsNullOrEmpty(m.Name));
|
||||
|
||||
var remoteSubtitleInfos = new List<RemoteSubtitleInfo>();
|
||||
|
||||
if (subtitles.Count() > 0)
|
||||
{
|
||||
foreach (var item in subtitles)
|
||||
{
|
||||
remoteSubtitleInfos.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(_jsonSerializer.SerializeToString(new DownloadSubInfo
|
||||
{
|
||||
Url = item.Url,
|
||||
Format = item.Ext,
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
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.Info("{0} Search | Summary -> Get {1} Subtitles", new object[2] { Name, remoteSubtitleInfos.Count });
|
||||
|
||||
return remoteSubtitleInfos;
|
||||
request.Language = "chi";
|
||||
}
|
||||
if (request.Language != "chi")
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var cid = await GetCidByFileAsync(request.MediaPath);
|
||||
|
||||
_logger.Info("{0} Search | FileHash -> {1}", new object[2] { Name, cid });
|
||||
|
||||
|
||||
HttpRequestOptions options = new HttpRequestOptions
|
||||
{
|
||||
Url = $"https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name={Path.GetFileName(request.MediaPath)}",
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
};
|
||||
var response = await _httpClient.GetResponse(options);
|
||||
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(response) });
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var subtitleResponse = _jsonSerializer.DeserializeFromStream<SubtitleResponseRoot>(response.Content);
|
||||
|
||||
if (subtitleResponse.Code == 0)
|
||||
{
|
||||
_logger.Info("{0} Search | Response -> {1}", new object[2] { Name, _jsonSerializer.SerializeToString(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.SerializeToString(new DownloadSubInfo
|
||||
{
|
||||
Url = item.Url,
|
||||
Format = item.Ext,
|
||||
Language = request.Language,
|
||||
IsForced = request.IsForced
|
||||
})),
|
||||
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.Info("{0} Search | Summary -> Get {1} Subtitles", new object[2] { Name, remoteSubtitles.Count });
|
||||
|
||||
return remoteSubtitles;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("{0} Search | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
_logger.Info("{0} Search | Summary -> Get 0 Subtitles", new object[1] { Name });
|
||||
@@ -150,11 +166,12 @@ namespace Emby.MeiamSub.Thunder
|
||||
|
||||
#region 下载字幕
|
||||
/// <summary>
|
||||
/// 下载请求
|
||||
/// 获取字幕内容 (ISubtitleProvider 接口实现)
|
||||
/// 根据字幕 ID 下载具体的字幕文件流。
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="id">字幕唯一标识符 (Base64 编码的 JSON 数据)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>包含字幕流的响应对象</returns>
|
||||
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Info("{0} DownloadSub | Request -> {1}", new object[2] { Name, id });
|
||||
@@ -169,36 +186,47 @@ namespace Emby.MeiamSub.Thunder
|
||||
/// <returns></returns>
|
||||
private async Task<SubtitleResponse> DownloadSubAsync(string info)
|
||||
{
|
||||
var downloadSub = _jsonSerializer.DeserializeFromString<DownloadSubInfo>(Base64Decode(info));
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
if (downloadSub == null)
|
||||
try
|
||||
{
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
var downloadSub = _jsonSerializer.DeserializeFromString<DownloadSubInfo>(Base64Decode(info));
|
||||
|
||||
_logger.Info("{0} DownloadSub | Url -> {1} | Format -> {2} | Language -> {3} ",
|
||||
new object[4] { Name, downloadSub.Url, downloadSub.Format, downloadSub.Language });
|
||||
|
||||
var response = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
Url = downloadSub.Url,
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
});
|
||||
|
||||
_logger.Info("{0} DownloadSub | Response -> {1}", new object[2] { Name, response.StatusCode });
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
|
||||
return new SubtitleResponse()
|
||||
if (downloadSub == null)
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = response.Content,
|
||||
};
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
|
||||
_logger.Info("{0} DownloadSub | Url -> {1} | Format -> {2} | Language -> {3} ",
|
||||
new object[4] { Name, downloadSub.Url, downloadSub.Format, downloadSub.Language });
|
||||
|
||||
var response = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
Url = downloadSub.Url,
|
||||
UserAgent = $"{Name}",
|
||||
TimeoutMs = 30000,
|
||||
AcceptHeader = "*/*",
|
||||
});
|
||||
|
||||
_logger.Info("{0} DownloadSub | Response -> {1}", new object[2] { Name, response.StatusCode });
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = response.Content,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("{0} DownloadSub | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
return new SubtitleResponse();
|
||||
@@ -236,53 +264,65 @@ namespace Emby.MeiamSub.Thunder
|
||||
/// <returns></returns>
|
||||
protected string ExtractFormat(string text)
|
||||
{
|
||||
|
||||
string result = null;
|
||||
|
||||
if (text != null)
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) result = ASS;
|
||||
else if (text.Contains(SSA)) result = SSA;
|
||||
else if (text.Contains(SRT)) result = SRT;
|
||||
else result = null;
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) return ASS;
|
||||
if (text.Contains(SSA)) return SSA;
|
||||
if (text.Contains(SRT)) return SRT;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 CID (迅雷)
|
||||
/// 异步计算文件 CID (迅雷专用算法)
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// <para>备注: 采用异步 I/O 读取文件特定位置的数据块进行 SHA1 计算。</para>
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
private string GetCidByFile(string filePath)
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <returns>计算得到的 CID 字符串</returns>
|
||||
private async Task<string> GetCidByFileAsync(string filePath)
|
||||
{
|
||||
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
var reader = new BinaryReader(stream);
|
||||
var fileSize = new FileInfo(filePath).Length;
|
||||
var SHA1 = new SHA1CryptoServiceProvider();
|
||||
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);
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 改造为异步方法,优化 I/O 性能,使用 SHA1.Create() 替代旧 API,并增加 using 语句释放资源
|
||||
|
||||
buffer = SHA1.ComputeHash(buffer, 0, 0xf000);
|
||||
}
|
||||
var result = "";
|
||||
foreach (var i in buffer)
|
||||
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous))
|
||||
{
|
||||
result += string.Format("{0:X2}", i);
|
||||
// 使用 BinaryReader 配合 BaseStream 需要小心,因为 BinaryReader 本身不支持异步 Read
|
||||
// 这里我们直接操作 stream 进行异步读取,不再使用 BinaryReader,因为只是读取字节数组
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.12.0</Version>
|
||||
<AssemblyVersion>1.0.12.0</AssemblyVersion>
|
||||
<FileVersion>1.0.12.0</FileVersion>
|
||||
@@ -12,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Jellyfin.Data.Entities.Libraries;
|
||||
using Jellyfin.MeiamSub.Shooter.Model;
|
||||
using Jellyfin.MeiamSub.Shooter.Model;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -24,7 +23,10 @@ using static System.Net.WebRequestMethods;
|
||||
namespace Jellyfin.MeiamSub.Shooter
|
||||
{
|
||||
/// <summary>
|
||||
/// 迅雷字幕组件
|
||||
/// 射手网字幕提供程序
|
||||
/// 负责与射手网 API 进行交互,通过文件哈希匹配并下载字幕。
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// </summary>
|
||||
public class ShooterProvider : ISubtitleProvider, IHasOrder
|
||||
{
|
||||
@@ -37,7 +39,7 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
|
||||
private static readonly HttpClient _httpClient = new HttpClient();
|
||||
|
||||
private string apiUrl => "https://www.shooter.cn/api/subapi.php";
|
||||
private const string ApiUrl = "https://www.shooter.cn/api/subapi.php";
|
||||
|
||||
public int Order => 1;
|
||||
|
||||
@@ -46,7 +48,7 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
/// <summary>
|
||||
/// 支持电影、剧集
|
||||
/// </summary>
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode };
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
@@ -61,11 +63,12 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
#region 查询字幕
|
||||
|
||||
/// <summary>
|
||||
/// 查询请求
|
||||
/// 搜索字幕 (ISubtitleProvider 接口实现)
|
||||
/// 根据媒体信息请求字幕列表。
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <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) }");
|
||||
@@ -82,79 +85,90 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
|
||||
{
|
||||
if (request.Language != "chi" && request.Language != "eng")
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
try
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new(request.MediaPath);
|
||||
|
||||
var hash = ComputeFileHash(fileInfo);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | FileHash -> { hash }");
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
{ "filehash", hash},
|
||||
{ "pathinfo", request.MediaPath},
|
||||
{ "format", "json"},
|
||||
{ "lang", request.Language == "chi" ? "chn" : "eng"}
|
||||
};
|
||||
|
||||
var content = new FormUrlEncodedContent(formData);
|
||||
|
||||
// 设置请求头
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||||
|
||||
// 发送 POST 请求
|
||||
var response = await _httpClient.PostAsync(apiUrl, content);
|
||||
|
||||
_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.LogInformation($"{Name} Search | ResponseBody -> { responseBody } ");
|
||||
|
||||
var subtitles = JsonSerializer.Deserialize<List<SubtitleResponseRoot>>(responseBody);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitles)}");
|
||||
|
||||
if (subtitles != null)
|
||||
if (request.Language != "chi" && request.Language != "eng")
|
||||
{
|
||||
|
||||
var remoteSubtitles = new List<RemoteSubtitleInfo>();
|
||||
|
||||
foreach (var subFileInfo in subtitles)
|
||||
{
|
||||
foreach (var subFile in subFileInfo.Files)
|
||||
{
|
||||
remoteSubtitles.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(JsonSerializer.Serialize(new DownloadSubInfo
|
||||
{
|
||||
Url = subFile.Link,
|
||||
Format = subFile.Ext,
|
||||
Language = request.Language,
|
||||
TwoLetterISOLanguageName = request.TwoLetterISOLanguageName,
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {Path.GetFileName(request.MediaPath)} | {request.TwoLetterISOLanguageName} | 射手",
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = subFile.Ext,
|
||||
Comment = $"Format : {ExtractFormat(subFile.Ext)}",
|
||||
IsHashMatch = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get {remoteSubtitles.Count} Subtitles");
|
||||
|
||||
return remoteSubtitles;
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new(request.MediaPath);
|
||||
|
||||
var hash = await ComputeFileHashAsync(fileInfo);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | FileHash -> {hash}");
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
{ "filehash", hash},
|
||||
{ "pathinfo", request.MediaPath},
|
||||
{ "format", "json"},
|
||||
{ "lang", request.Language == "chi" ? "chn" : "eng"}
|
||||
};
|
||||
|
||||
var content = new FormUrlEncodedContent(formData);
|
||||
|
||||
// 设置请求头
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
|
||||
|
||||
// 发送 POST 请求
|
||||
var response = await _httpClient.PostAsync(ApiUrl, content);
|
||||
|
||||
_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.LogInformation($"{Name} Search | ResponseBody -> {responseBody} ");
|
||||
|
||||
var subtitles = JsonSerializer.Deserialize<List<SubtitleResponseRoot>>(responseBody);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Response -> {JsonSerializer.Serialize(subtitles)}");
|
||||
|
||||
if (subtitles != null)
|
||||
{
|
||||
|
||||
var remoteSubtitles = new List<RemoteSubtitleInfo>();
|
||||
|
||||
foreach (var subFileInfo in subtitles)
|
||||
{
|
||||
foreach (var subFile in subFileInfo.Files)
|
||||
{
|
||||
remoteSubtitles.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Id = Base64Encode(JsonSerializer.Serialize(new DownloadSubInfo
|
||||
{
|
||||
Url = subFile.Link,
|
||||
Format = subFile.Ext,
|
||||
Language = request.Language,
|
||||
TwoLetterISOLanguageName = request.TwoLetterISOLanguageName,
|
||||
})),
|
||||
Name = $"[MEIAMSUB] {Path.GetFileName(request.MediaPath)} | {request.TwoLetterISOLanguageName} | 射手",
|
||||
Author = "Meiam ",
|
||||
ProviderName = $"{Name}",
|
||||
Format = subFile.Ext,
|
||||
Comment = $"Format : {ExtractFormat(subFile.Ext)}",
|
||||
IsHashMatch = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get {remoteSubtitles.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");
|
||||
@@ -165,11 +179,12 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
|
||||
#region 下载字幕
|
||||
/// <summary>
|
||||
/// 下载请求
|
||||
/// 获取字幕内容 (ISubtitleProvider 接口实现)
|
||||
/// 根据字幕 ID 下载具体的字幕文件流。
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="id">字幕唯一标识符 (Base64 编码的 JSON 数据)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>包含字幕流的响应对象</returns>
|
||||
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"{Name} DownloadSub | Request -> {id}");
|
||||
@@ -184,41 +199,52 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
/// <returns></returns>
|
||||
private async Task<SubtitleResponse> DownloadSubAsync(string info)
|
||||
{
|
||||
var downloadSub = JsonSerializer.Deserialize<DownloadSubInfo>(Base64Decode(info));
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
if (downloadSub == null)
|
||||
try
|
||||
{
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
var downloadSub = JsonSerializer.Deserialize<DownloadSubInfo>(Base64Decode(info));
|
||||
|
||||
_logger.LogInformation($"{Name} DownloadSub | Url -> { downloadSub.Url } | Format -> { downloadSub.Format } | Language -> { downloadSub.Language } ");
|
||||
if (downloadSub == null)
|
||||
{
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(downloadSub.Url),
|
||||
Headers =
|
||||
_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 }");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = stream,
|
||||
};
|
||||
|
||||
var response = await _httpClient.SendAsync(options);
|
||||
|
||||
_logger.LogInformation($"{Name} DownloadSub | Response -> {response.StatusCode}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = stream,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{0} DownloadSub | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
return new SubtitleResponse();
|
||||
@@ -257,27 +283,33 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
/// <returns></returns>
|
||||
protected string ExtractFormat(string text)
|
||||
{
|
||||
|
||||
string result = null;
|
||||
|
||||
if (text != null)
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) result = ASS;
|
||||
else if (text.Contains(SSA)) result = SSA;
|
||||
else if (text.Contains(SRT)) result = SRT;
|
||||
else result = null;
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) return ASS;
|
||||
if (text.Contains(SSA)) return SSA;
|
||||
if (text.Contains(SRT)) return SRT;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 Hash (射手)
|
||||
/// 异步计算文件 Hash (射手网专用算法)
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// <para>备注: 采用异步 I/O 读取文件特定位置的 4KB 数据块进行 MD5 计算。</para>
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public static string ComputeFileHash(FileInfo fileInfo)
|
||||
/// <param name="fileInfo">文件信息对象</param>
|
||||
/// <returns>计算得到的文件 Hash 字符串,如果文件过小或不存在则返回空字符串</returns>
|
||||
public static async Task<string> ComputeFileHashAsync(FileInfo fileInfo)
|
||||
{
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 改造为异步方法,优化 I/O 性能并增加 using 语句释放资源
|
||||
|
||||
string ret = "";
|
||||
|
||||
if (!fileInfo.Exists || fileInfo.Length < 8 * 1024)
|
||||
@@ -285,47 +317,44 @@ namespace Jellyfin.MeiamSub.Shooter
|
||||
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)
|
||||
using (FileStream fs = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous))
|
||||
{
|
||||
fs.Seek(offset[i], 0);
|
||||
fs.Read(bBuf, 0, 4 * 1024);
|
||||
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;
|
||||
|
||||
MD5 md5Hash = MD5.Create();
|
||||
byte[] data = md5Hash.ComputeHash(bBuf);
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
byte[] bBuf = new byte[1024 * 4];
|
||||
|
||||
for (int j = 0; j < data.Length; j++)
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
sBuilder.Append(data[j].ToString("x2"));
|
||||
}
|
||||
fs.Seek(offset[i], SeekOrigin.Begin);
|
||||
await fs.ReadExactlyAsync(bBuf, 0, 4 * 1024);
|
||||
|
||||
if (!string.IsNullOrEmpty(ret))
|
||||
{
|
||||
ret += ";";
|
||||
}
|
||||
using (MD5 md5Hash = MD5.Create())
|
||||
{
|
||||
byte[] data = md5Hash.ComputeHash(bBuf);
|
||||
StringBuilder sBuilder = new StringBuilder();
|
||||
|
||||
ret += sBuilder.ToString();
|
||||
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;
|
||||
}
|
||||
|
||||
public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.12.0</Version>
|
||||
<AssemblyVersion>1.0.12.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
@@ -14,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.3" />
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
||||
@@ -19,7 +19,10 @@ 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
|
||||
{
|
||||
@@ -41,7 +44,7 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
/// <summary>
|
||||
/// 支持电影、剧集
|
||||
/// </summary>
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
|
||||
public IEnumerable<VideoContentType> SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode };
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
@@ -56,11 +59,12 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
#region 查询字幕
|
||||
|
||||
/// <summary>
|
||||
/// 查询请求
|
||||
/// 搜索字幕 (ISubtitleProvider 接口实现)
|
||||
/// 根据媒体信息请求字幕列表。
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <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) }");
|
||||
@@ -77,70 +81,81 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
/// <returns></returns>
|
||||
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
|
||||
{
|
||||
if (request.Language != "chi")
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
try
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
if (request.Language != "chi")
|
||||
{
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var cid = GetCidByFile(request.MediaPath);
|
||||
var cid = await GetCidByFileAsync(request.MediaPath);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | FileHash -> { cid }");
|
||||
_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)}"),
|
||||
Headers =
|
||||
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 response = await _httpClient.SendAsync(options);
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Response -> { JsonSerializer.Serialize(response) }");
|
||||
_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)
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogInformation($"{Name} Search | Response -> { JsonSerializer.Serialize(subtitleResponse) }");
|
||||
var subtitleResponse = JsonSerializer.Deserialize<SubtitleResponseRoot>(await response.Content.ReadAsStringAsync(), _deserializeOptions);
|
||||
|
||||
var subtitles = subtitleResponse.Data.Where(m => !string.IsNullOrEmpty(m.Name));
|
||||
|
||||
var remoteSubtitleInfos = new List<RemoteSubtitleInfo>();
|
||||
|
||||
if (subtitles.Count() > 0)
|
||||
if (subtitleResponse != null)
|
||||
{
|
||||
foreach (var item in subtitles)
|
||||
_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)
|
||||
{
|
||||
remoteSubtitleInfos.Add(new RemoteSubtitleInfo()
|
||||
foreach (var item in subtitles)
|
||||
{
|
||||
Id = Base64Encode(JsonSerializer.Serialize(new DownloadSubInfo
|
||||
remoteSubtitles.Add(new RemoteSubtitleInfo()
|
||||
{
|
||||
Url = item.Url,
|
||||
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,
|
||||
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,
|
||||
});
|
||||
Comment = $"Format : {item.Ext}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get {subtitles.Count()} Subtitles");
|
||||
|
||||
return remoteSubtitles;
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get {subtitles.Count()} Subtitles");
|
||||
|
||||
return remoteSubtitleInfos;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{0} Search | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles");
|
||||
|
||||
@@ -150,11 +165,12 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
|
||||
#region 下载字幕
|
||||
/// <summary>
|
||||
/// 下载请求
|
||||
/// 获取字幕内容 (ISubtitleProvider 接口实现)
|
||||
/// 根据字幕 ID 下载具体的字幕文件流。
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="id">字幕唯一标识符 (Base64 编码的 JSON 数据)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>包含字幕流的响应对象</returns>
|
||||
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation($"{Name} DownloadSub | Request -> {id}");
|
||||
@@ -169,41 +185,52 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
/// <returns></returns>
|
||||
private async Task<SubtitleResponse> DownloadSubAsync(string info)
|
||||
{
|
||||
var downloadSub = JsonSerializer.Deserialize<DownloadSubInfo>(Base64Decode(info));
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 增加异常处理
|
||||
|
||||
if (downloadSub == null)
|
||||
try
|
||||
{
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
var downloadSub = JsonSerializer.Deserialize<DownloadSubInfo>(Base64Decode(info));
|
||||
|
||||
_logger.LogInformation($"{Name} DownloadSub | Url -> { downloadSub.Url } | Format -> { downloadSub.Format } | Language -> { downloadSub.Language } ");
|
||||
if (downloadSub == null)
|
||||
{
|
||||
return new SubtitleResponse();
|
||||
}
|
||||
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
Method = HttpMethod.Get,
|
||||
RequestUri = new Uri(downloadSub.Url),
|
||||
Headers =
|
||||
_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 }");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = stream,
|
||||
};
|
||||
|
||||
var response = await _httpClient.SendAsync(options);
|
||||
|
||||
_logger.LogInformation($"{Name} DownloadSub | Response -> {response.StatusCode}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
Language = downloadSub.Language,
|
||||
IsForced = false,
|
||||
Format = downloadSub.Format,
|
||||
Stream = stream,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{0} DownloadSub | Error -> {1}", Name, ex.Message);
|
||||
}
|
||||
|
||||
return new SubtitleResponse();
|
||||
@@ -241,53 +268,62 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
/// <returns></returns>
|
||||
protected string ExtractFormat(string text)
|
||||
{
|
||||
|
||||
string result = null;
|
||||
|
||||
if (text != null)
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) result = ASS;
|
||||
else if (text.Contains(SSA)) result = SSA;
|
||||
else if (text.Contains(SRT)) result = SRT;
|
||||
else result = null;
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
|
||||
text = text.ToLower();
|
||||
if (text.Contains(ASS)) return ASS;
|
||||
if (text.Contains(SSA)) return SSA;
|
||||
if (text.Contains(SRT)) return SRT;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文件 CID (迅雷)
|
||||
/// 异步计算文件 CID (迅雷专用算法)
|
||||
/// <para>修改人: Meiam</para>
|
||||
/// <para>修改时间: 2025-12-22</para>
|
||||
/// <para>备注: 采用异步 I/O 读取文件特定位置的数据块进行 SHA1 计算。</para>
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
private string GetCidByFile(string filePath)
|
||||
/// <param name="filePath">文件路径</param>
|
||||
/// <returns>计算得到的 CID 字符串</returns>
|
||||
private async Task<string> GetCidByFileAsync(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);
|
||||
// 修改人: Meiam
|
||||
// 修改时间: 2025-12-22
|
||||
// 备注: 改造为异步方法,优化 I/O 性能,使用 SHA1.Create() 替代旧 API,并增加 using 语句释放资源
|
||||
|
||||
buffer = sha1.ComputeHash(buffer, 0, 0xf000);
|
||||
}
|
||||
var result = "";
|
||||
foreach (var i in buffer)
|
||||
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous))
|
||||
{
|
||||
result += string.Format("{0:X2}", i);
|
||||
var fileSize = new FileInfo(filePath).Length;
|
||||
using (var sha1 = SHA1.Create())
|
||||
{
|
||||
var buffer = new byte[0xf000];
|
||||
if (fileSize < 0xf000)
|
||||
{
|
||||
await stream.ReadExactlyAsync(buffer, 0, (int)fileSize);
|
||||
buffer = sha1.ComputeHash(buffer, 0, (int)fileSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
await stream.ReadExactlyAsync(buffer, 0, 0x5000);
|
||||
stream.Seek(fileSize / 3, SeekOrigin.Begin);
|
||||
await stream.ReadExactlyAsync(buffer, 0x5000, 0x5000);
|
||||
stream.Seek(fileSize - 0x5000, SeekOrigin.Begin);
|
||||
await stream.ReadExactlyAsync(buffer, 0xa000, 0x5000);
|
||||
|
||||
buffer = sha1.ComputeHash(buffer, 0, 0xf000);
|
||||
}
|
||||
var result = "";
|
||||
foreach (var i in buffer)
|
||||
{
|
||||
result += string.Format("{0:X2}", i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user