修复: 支持 Assrt 压缩字幕下载

This commit is contained in:
Meiam
2026-08-07 11:56:46 +08:00
parent 220fe5459d
commit 38a46d2f53
7 changed files with 63 additions and 65 deletions

View File

@@ -8,12 +8,12 @@ using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using System.Net.Http;
namespace Emby.MeiamSub.Assrt
@@ -147,7 +147,7 @@ namespace Emby.MeiamSub.Assrt
if (item.id == null) continue;
var subtitleId = item.id.ToString();
var format = ExtractFormat(item.subtype) ?? ExtractFormat(item.name) ?? SRT;
var format = ExtractFormat(item.subtype) ?? ExtractFormat(item.native_name) ?? SRT;
var langDesc = item.lang?.desc ?? "中文";
@@ -163,7 +163,7 @@ namespace Emby.MeiamSub.Assrt
remoteSubtitles.Add(new RemoteSubtitleInfo
{
Id = encodedId,
Name = $"[MEIAMSUB] {item.name} | {langDesc} | 伪射手",
Name = $"[MEIAMSUB] {item.native_name ?? item.videoname ?? subtitleId} | {langDesc} | 伪射手",
Author = "Assrt",
ProviderName = Name,
Format = format,
@@ -230,6 +230,12 @@ namespace Emby.MeiamSub.Assrt
var detailResponse = _jsonSerializer.DeserializeFromString<AssrtDetailResponse>(detailJson);
if (detailResponse == null || detailResponse.status != 0)
{
_logger.Error($"{Name} GetSubtitles | Detail API error. Status: {detailResponse?.status}, Response: {detailJson}");
return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() };
}
var targetSub = detailResponse?.sub?.subs?.FirstOrDefault();
if (targetSub == null || string.IsNullOrEmpty(targetSub.url))
{
@@ -237,13 +243,16 @@ namespace Emby.MeiamSub.Assrt
return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() };
}
_logger.Info($"{Name} GetSubtitles | Downloading subtitle from: {targetSub.url}");
var downloadUrl = targetSub.url;
var downloadFilename = targetSub.filename;
_logger.Info($"{Name} GetSubtitles | Downloading subtitle from: {downloadUrl}");
// 2. 下载字幕流 (手动接管重定向以确保每次重定向请求均带有正确的防盗链 Headers)
var handler = new HttpClientHandler { AllowAutoRedirect = false };
using var cleanHttpClient = new HttpClient(handler);
var currentUrl = targetSub.url;
var currentUrl = downloadUrl;
HttpResponseMessage downloadResponse = null;
int redirectCount = 0;
@@ -288,46 +297,33 @@ namespace Emby.MeiamSub.Assrt
var responseStream = await downloadResponse.Content.ReadAsStreamAsync();
// 3. 防御性处理:检查是否为 ZIP 压缩包并提取
// 3. 下载原始文件;若为压缩包,则从 ZIP/RAR/7z/TAR 等归档中提取字幕。
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
bool isZip = false;
if (memoryStream.Length > 4)
var directFormat = ExtractFormat(downloadFilename);
if (directFormat == null)
{
byte[] zipHeader = new byte[4];
memoryStream.Read(zipHeader, 0, 4);
if (zipHeader[0] == 0x50 && zipHeader[1] == 0x4B && zipHeader[2] == 0x03 && zipHeader[3] == 0x04)
{
isZip = true;
}
memoryStream.Position = 0;
}
if (isZip || targetSub.filename?.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) == true)
{
_logger.Info($"{Name} GetSubtitles | ZIP file detected, extracting...");
_logger.Info($"{Name} GetSubtitles | Archive detected, extracting: {downloadFilename}");
try
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true))
using (var archive = ArchiveFactory.Open(memoryStream))
{
var entry = archive.Entries.FirstOrDefault(e =>
e.FullName.EndsWith(".srt", StringComparison.OrdinalIgnoreCase) ||
e.FullName.EndsWith(".ass", StringComparison.OrdinalIgnoreCase) ||
e.FullName.EndsWith(".ssa", StringComparison.OrdinalIgnoreCase));
!e.IsDirectory && ExtractFormat(e.Key) != null);
if (entry != null)
{
var extractedStream = new MemoryStream();
using (var entryStream = entry.Open())
using (var entryStream = entry.OpenEntryStream())
{
await entryStream.CopyToAsync(extractedStream);
}
extractedStream.Position = 0;
var extFormat = ExtractFormat(entry.FullName) ?? downloadInfo.Format;
var extFormat = ExtractFormat(entry.Key) ?? downloadInfo.Format;
_logger.Info($"{Name} GetSubtitles | Extracted file: '{entry.FullName}' with format: {extFormat}");
_logger.Info($"{Name} GetSubtitles | Extracted file: '{entry.Key}' with format: {extFormat}");
return new SubtitleResponse
{
@@ -339,23 +335,25 @@ namespace Emby.MeiamSub.Assrt
}
else
{
_logger.Warn($"{Name} GetSubtitles | No srt/ass/ssa file found inside the ZIP package.");
_logger.Warn($"{Name} GetSubtitles | Archive contains no supported srt/ass/ssa subtitle.");
return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() };
}
}
}
catch (Exception ex)
{
_logger.Error($"{Name} GetSubtitles | Failed to extract ZIP archive: {ex.Message}");
_logger.Error($"{Name} GetSubtitles | Failed to extract archive '{downloadFilename}': {ex.Message}");
return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() };
}
}
// 4. 不是 ZIP 或者解压失败时,默认返回原流
// 4. 非压缩的字幕文件直接返回。
memoryStream.Position = 0;
return new SubtitleResponse
{
Language = downloadInfo.Language,
IsForced = false,
Format = downloadInfo.Format,
Format = directFormat ?? downloadInfo.Format,
Stream = memoryStream
};
}

View File

@@ -25,6 +25,7 @@
<ItemGroup>
<PackageReference Include="MediaBrowser.Common" Version="4.9.1.90" />
<PackageReference Include="MediaBrowser.Server.Core" Version="4.9.1.90" />
<PackageReference Include="SharpCompress" Version="0.33.0" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@@ -18,7 +18,7 @@ namespace Emby.MeiamSub.Assrt.Model
public class AssrtSearchItem
{
public object id { get; set; }
public string name { get; set; }
public string native_name { get; set; }
public string videoname { get; set; }
public string subtype { get; set; }
public AssrtLanguageInfo lang { get; set; }

View File

@@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -14,6 +13,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
namespace Jellyfin.MeiamSub.Assrt
{
@@ -154,7 +154,7 @@ namespace Jellyfin.MeiamSub.Assrt
remoteSubtitles.Add(new RemoteSubtitleInfo
{
Id = encodedId,
Name = $"[MEIAMSUB] {item.Name} | {langDesc} | 伪射手",
Name = $"[MEIAMSUB] {item.Name ?? item.VideoName ?? subtitleId} | {langDesc} | 伪射手",
Author = "Assrt",
ProviderName = Name,
Format = format,
@@ -209,6 +209,12 @@ namespace Jellyfin.MeiamSub.Assrt
var detailJson = await detailResponseMsg.Content.ReadAsStringAsync(cancellationToken);
var detailResponse = JsonSerializer.Deserialize<AssrtDetailResponse>(detailJson);
if (detailResponse == null || detailResponse.Status != 0)
{
_logger.LogError($"{Name} GetSubtitles | Detail API error. Status: {detailResponse?.Status}, Response: {detailJson}");
throw new InvalidOperationException($"{Name} GetSubtitles | Assrt API returned error status: {detailResponse?.Status}");
}
var targetSub = detailResponse?.Sub?.Subs?.FirstOrDefault();
if (targetSub == null || string.IsNullOrEmpty(targetSub.Url))
{
@@ -216,13 +222,16 @@ namespace Jellyfin.MeiamSub.Assrt
throw new InvalidOperationException($"{Name} GetSubtitles | API 响应中未找到字幕下载地址。");
}
_logger.LogInformation($"{Name} GetSubtitles | Downloading subtitle from: {targetSub.Url}");
var downloadUrl = targetSub.Url;
var downloadFilename = targetSub.Filename;
_logger.LogInformation($"{Name} GetSubtitles | Downloading subtitle from: {downloadUrl}");
// 2. 下载字幕流 (手动接管重定向以确保每次重定向请求均带有正确的防盗链 Headers)
var handler = new HttpClientHandler { AllowAutoRedirect = false };
using var cleanHttpClient = new HttpClient(handler);
var currentUrl = targetSub.Url;
var currentUrl = downloadUrl;
HttpResponseMessage downloadResponse = null;
int redirectCount = 0;
@@ -269,47 +278,32 @@ namespace Jellyfin.MeiamSub.Assrt
var responseStream = await downloadResponse.Content.ReadAsStreamAsync(cancellationToken);
// 3. 防御性处理:检查是否为 ZIP 压缩包
// 如果是 ZIP,解压出里面第一个符合格式要求的字幕流
// 3. 下载原始文件;若为压缩包,则从 ZIP/RAR/7z/TAR 等归档中提取字幕。
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
// 判断 ZIP 标志 (PK.. / 0x50 0x4B)
bool isZip = false;
if (memoryStream.Length > 4)
var directFormat = ExtractFormat(downloadFilename);
if (directFormat == null)
{
byte[] zipHeader = new byte[4];
await memoryStream.ReadExactlyAsync(zipHeader, 0, 4, cancellationToken);
if (zipHeader[0] == 0x50 && zipHeader[1] == 0x4B && zipHeader[2] == 0x03 && zipHeader[3] == 0x04)
{
isZip = true;
}
memoryStream.Position = 0;
}
if (isZip || targetSub.Filename?.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) == true)
{
_logger.LogInformation($"{Name} GetSubtitles | ZIP file detected, extracting...");
_logger.LogInformation($"{Name} GetSubtitles | Archive detected, extracting: {downloadFilename}");
try
{
using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true);
using var archive = ArchiveFactory.OpenArchive(memoryStream);
var entry = archive.Entries.FirstOrDefault(e =>
e.FullName.EndsWith(".srt", StringComparison.OrdinalIgnoreCase) ||
e.FullName.EndsWith(".ass", StringComparison.OrdinalIgnoreCase) ||
e.FullName.EndsWith(".ssa", StringComparison.OrdinalIgnoreCase));
!e.IsDirectory && ExtractFormat(e.Key) != null);
if (entry != null)
{
var extractedStream = new MemoryStream();
using (var entryStream = entry.Open())
using (var entryStream = entry.OpenEntryStream())
{
await entryStream.CopyToAsync(extractedStream, cancellationToken);
}
extractedStream.Position = 0;
var extFormat = ExtractFormat(entry.FullName) ?? downloadInfo.Format;
var extFormat = ExtractFormat(entry.Key) ?? downloadInfo.Format;
_logger.LogInformation($"{Name} GetSubtitles | Extracted file: '{entry.FullName}' with format: {extFormat}");
_logger.LogInformation($"{Name} GetSubtitles | Extracted file: '{entry.Key}' with format: {extFormat}");
return new SubtitleResponse
{
@@ -321,22 +315,23 @@ namespace Jellyfin.MeiamSub.Assrt
}
else
{
_logger.LogWarning($"{Name} GetSubtitles | No srt/ass/ssa file found inside the ZIP package.");
throw new InvalidOperationException($"{Name} GetSubtitles | Archive contains no supported srt/ass/ssa subtitle.");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{Name} GetSubtitles | Failed to extract ZIP archive: {ex.Message}");
_logger.LogError(ex, $"{Name} GetSubtitles | Failed to extract archive '{downloadFilename}': {ex.Message}");
throw;
}
}
// 4. 不是 ZIP 或者解压失败时,默认返回原流
// 4. 非压缩的字幕文件直接返回。
memoryStream.Position = 0;
return new SubtitleResponse
{
Language = downloadInfo.Language,
IsForced = false,
Format = downloadInfo.Format,
Format = directFormat ?? downloadInfo.Format,
Stream = memoryStream
};
}

View File

@@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.11.11" />
<PackageReference Include="SharpCompress" Version="0.50.1" GeneratePathProperty="true" />
</ItemGroup>
<ItemGroup>
@@ -28,6 +29,7 @@
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(SolutionDir)$(ConfigurationName)\" SkipUnchangedFiles="false" />
<Copy SourceFiles="$(PkgSharpCompress)\lib\net8.0\SharpCompress.dll" DestinationFolder="$(SolutionDir)$(ConfigurationName)\dependencies\Jellyfin\" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -32,7 +32,7 @@ namespace Jellyfin.MeiamSub.Assrt.Model
[JsonPropertyName("id")]
public object Id { get; set; } // 可以是数字也可以是字符串,用 object 接收再 ToString()
[JsonPropertyName("name")]
[JsonPropertyName("native_name")]
public string Name { get; set; }
[JsonPropertyName("videoname")]
@@ -82,6 +82,7 @@ namespace Jellyfin.MeiamSub.Assrt.Model
[JsonPropertyName("url")]
public string Url { get; set; }
}
/// <summary>

View File

@@ -130,6 +130,7 @@ Copy-Item Jellyfin.MeiamSub.Thunder\thumb.png $JellyfinThunderPackDir\
# Copy Jellyfin Assrt artifacts
Copy-Item Release\Jellyfin.MeiamSub.Assrt.dll $JellyfinAssrtPackDir\
Copy-Item Release\dependencies\Jellyfin\* $JellyfinAssrtPackDir\
Copy-Item Jellyfin.MeiamSub.Assrt\meta.json $JellyfinAssrtPackDir\
Copy-Item Jellyfin.MeiamSub.Assrt\thumb.png $JellyfinAssrtPackDir\