mirror of
https://github.com/91270/MeiamSubtitles.git
synced 2026-11-04 22:45:51 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb44362717 | ||
|
|
9455450cf2 | ||
|
|
c473839f4d | ||
|
|
46bc076b8f | ||
|
|
7ec1583bd0 | ||
|
|
bf6304df96 | ||
|
|
87a5c361b8 | ||
|
|
dba34a6231 | ||
|
|
1fe2a70221 |
@@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>1.0.16.0</AssemblyVersion>
|
||||
<FileVersion>1.0.16.0</FileVersion>
|
||||
<Version>1.0.16.0</Version>
|
||||
<AssemblyVersion>1.0.17.0</AssemblyVersion>
|
||||
<FileVersion>1.0.17.0</FileVersion>
|
||||
<Version>1.0.17.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<!-- Emby 4.9 provides SharpCompress 0.33.0 at runtime. Upgrading the compile-time
|
||||
reference would make the plugin request an assembly version the host does not provide. -->
|
||||
<!-- 已验证 Emby 4.8.10.0、4.9.5.0 和 4.10.0.40 均提供 SharpCompress 0.33.0。
|
||||
保持与宿主程序集版本一致,避免插件加载或解压失败。 -->
|
||||
<NoWarn>$(NoWarn);NU1902</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.9.1.90" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.9.1.90" />
|
||||
<!-- 使用旧版宿主可解析的 SDK,避免插件要求 4.9.1.90 程序集。 -->
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.8.10" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.8.10" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" ExcludeAssets="runtime" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyVersion>1.0.16.0</AssemblyVersion>
|
||||
<FileVersion>1.0.16.0</FileVersion>
|
||||
<Version>1.0.16.0</Version>
|
||||
<AssemblyVersion>1.0.17.0</AssemblyVersion>
|
||||
<FileVersion>1.0.17.0</FileVersion>
|
||||
<Version>1.0.17.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -24,8 +24,9 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.9.1.90" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.9.1.90" />
|
||||
<!-- 使用旧版宿主可解析的 SDK,避免插件要求 4.9.1.90 程序集。 -->
|
||||
<PackageReference Include="MediaBrowser.Common" Version="4.8.10" />
|
||||
<PackageReference Include="MediaBrowser.Server.Core" Version="4.8.10" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -33,6 +34,10 @@
|
||||
<Folder Include="Configuration\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\SubtitleContentValidator.cs" Link="Shared\SubtitleContentValidator.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(SolutionDir)$(ConfigurationName)\" SkipUnchangedFiles="false" />
|
||||
</Target>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Emby.MeiamSub.Thunder.Model;
|
||||
using MeiamSubtitles.Shared;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Base;
|
||||
@@ -76,7 +77,13 @@ namespace Emby.MeiamSub.Thunder
|
||||
/// <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) });
|
||||
if (request == null)
|
||||
{
|
||||
_logger.Info("{0} Search | Request is null, skip search.", Name);
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
_logger.Info("{0} Search | ContentType -> {1} | Language -> {2}", Name, request.ContentType, request.Language);
|
||||
|
||||
var subtitles = await SearchSubtitlesAsync(request, cancellationToken);
|
||||
|
||||
@@ -130,11 +137,25 @@ namespace Emby.MeiamSub.Thunder
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var cid = string.Empty;
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
var cid = await GetCidByFileAsync(request.MediaPath, cancellationToken);
|
||||
stopWatch.Stop();
|
||||
|
||||
_logger.Info("{0} Search | FileHash -> {1} (Took {2}ms)", new object[3] { Name, cid, stopWatch.ElapsedMilliseconds });
|
||||
try
|
||||
{
|
||||
cid = await GetCidByFileAsync(request.MediaPath, cancellationToken);
|
||||
_logger.Info("{0} Search | FileHash -> {1} (Took {2}ms)", Name, cid, stopWatch.ElapsedMilliseconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||
{
|
||||
_logger.Warn("{0} Search | File hash unavailable ({1}: {2}), continue with name search.", Name, ex.GetType().Name, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopWatch.Stop();
|
||||
}
|
||||
|
||||
|
||||
HttpRequestOptions options = new HttpRequestOptions
|
||||
@@ -186,7 +207,7 @@ namespace Emby.MeiamSub.Thunder
|
||||
ProviderName = $"{Name}",
|
||||
Format = NormalizeFormat(item.Ext),
|
||||
Comment = $"Format : {NormalizeFormat(item.Ext)}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
IsHashMatch = !string.IsNullOrEmpty(cid) && string.Equals(cid, item.Cid, StringComparison.OrdinalIgnoreCase),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -265,7 +286,7 @@ namespace Emby.MeiamSub.Thunder
|
||||
await response.Content.CopyToAsync(stream, 81920, cancellationToken);
|
||||
var data = stream.ToArray();
|
||||
stream.Dispose();
|
||||
ValidateSubtitleData(data, format, response.ContentType);
|
||||
SubtitleContentValidator.Validate(data, format, response.ContentType);
|
||||
|
||||
return new SubtitleResponse()
|
||||
{
|
||||
@@ -335,38 +356,6 @@ namespace Emby.MeiamSub.Thunder
|
||||
return value == SRT || value == ASS || value == SSA ? value : null;
|
||||
}
|
||||
|
||||
private static void ValidateSubtitleData(byte[] data, string format, string mediaType)
|
||||
{
|
||||
if (data == null || data.Length < 8)
|
||||
{
|
||||
throw new InvalidDataException("Subtitle response is empty.");
|
||||
}
|
||||
|
||||
if (data[0] == (byte)'P' && data[1] == (byte)'K' ||
|
||||
data.Length >= 7 && Encoding.ASCII.GetString(data, 0, 7) == "Rar!\u001a\u0007")
|
||||
{
|
||||
throw new InvalidDataException("Compressed subtitle responses are not supported by Thunder.");
|
||||
}
|
||||
|
||||
var sample = Encoding.UTF8.GetString(data, 0, Math.Min(data.Length, 4096)).TrimStart('\uFEFF', ' ', '\r', '\n', '\t');
|
||||
if (sample.StartsWith("<", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("{", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("[", StringComparison.Ordinal) ||
|
||||
mediaType?.IndexOf("html", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
mediaType?.IndexOf("json", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
throw new InvalidDataException("Thunder returned an error document instead of a subtitle.");
|
||||
}
|
||||
|
||||
var valid = format == SRT
|
||||
? sample.IndexOf("-->", StringComparison.Ordinal) >= 0
|
||||
: sample.IndexOf("[Script Info]", StringComparison.OrdinalIgnoreCase) >= 0 || sample.IndexOf("Dialogue:", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
if (!valid)
|
||||
{
|
||||
throw new InvalidDataException($"Downloaded content is not a valid {format} subtitle.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规范化语言代码
|
||||
/// </summary>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.16.0</Version>
|
||||
<AssemblyVersion>1.0.16.0</AssemblyVersion>
|
||||
<FileVersion>1.0.16.0</FileVersion>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Version>1.0.17.0</Version>
|
||||
<AssemblyVersion>1.0.17.0</AssemblyVersion>
|
||||
<FileVersion>1.0.17.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
@@ -13,7 +13,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.11" />
|
||||
<!-- 使用已验证的最低兼容基线,保持同一 DLL 可运行于 Jellyfin 10.10.7、10.11.11 和 12.1。 -->
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.7" />
|
||||
<PackageReference Include="SharpCompress" Version="0.50.1" GeneratePathProperty="true" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"guid": "E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0",
|
||||
"name": "MeiamSub.Assrt",
|
||||
"owner": "Meiam",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"timestamp": "2026-08-07T07:11:29.0000000Z",
|
||||
"version": "1.0.16.0",
|
||||
"targetAbi": "10.10.7.0",
|
||||
"timestamp": "2026-09-16T08:13:02.0000000Z",
|
||||
"version": "1.0.17.0",
|
||||
"status": "Active",
|
||||
"imagePath": "thumb.png"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Version>1.0.16.0</Version>
|
||||
<AssemblyVersion>1.0.16.0</AssemblyVersion>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Version>1.0.17.0</Version>
|
||||
<AssemblyVersion>1.0.17.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
@@ -12,7 +12,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.11.11" />
|
||||
<!-- 使用已验证的最低兼容基线,保持同一 DLL 可运行于 Jellyfin 10.10.7、10.11.11 和 12.1。 -->
|
||||
<PackageReference Include="Jellyfin.Controller" Version="10.10.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -21,6 +22,10 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\SubtitleContentValidator.cs" Link="Shared\SubtitleContentValidator.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Jellyfin.MeiamSub.Thunder.Model;
|
||||
using MeiamSubtitles.Shared;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Providers;
|
||||
@@ -135,11 +136,27 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
return Array.Empty<RemoteSubtitleInfo>();
|
||||
}
|
||||
|
||||
var cid = string.Empty;
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
var cid = await GetCidByFileAsync(request.MediaPath, cancellationToken);
|
||||
stopWatch.Stop();
|
||||
|
||||
_logger.LogInformation(Name + " Search | FileHash -> " + cid + " (Took " + stopWatch.ElapsedMilliseconds + "ms)");
|
||||
try
|
||||
{
|
||||
cid = await GetCidByFileAsync(request.MediaPath, cancellationToken);
|
||||
_logger.LogInformation("{Provider} Search | FileHash -> {FileHash} (Took {ElapsedMilliseconds}ms)",
|
||||
Name, cid, stopWatch.ElapsedMilliseconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning("{Provider} Search | File hash unavailable ({ExceptionType}: {Message}), continue with name search.",
|
||||
Name, ex.GetType().Name, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopWatch.Stop();
|
||||
}
|
||||
|
||||
using var options = new HttpRequestMessage
|
||||
{
|
||||
@@ -190,7 +207,7 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
ProviderName = $"{Name}",
|
||||
Format = NormalizeFormat(item.Ext),
|
||||
Comment = $"Format : {NormalizeFormat(item.Ext)}",
|
||||
IsHashMatch = cid == item.Cid,
|
||||
IsHashMatch = !string.IsNullOrEmpty(cid) && string.Equals(cid, item.Cid, StringComparison.OrdinalIgnoreCase),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -266,7 +283,7 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
var format = NormalizeFormat(downloadSub.Format)
|
||||
?? throw new InvalidDataException($"Unsupported subtitle format: {downloadSub.Format}");
|
||||
var data = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
ValidateSubtitleData(data, format, response.Content.Headers.ContentType?.MediaType);
|
||||
SubtitleContentValidator.Validate(data, format, response.Content.Headers.ContentType?.MediaType);
|
||||
var stream = new MemoryStream(data, writable: false);
|
||||
|
||||
return new SubtitleResponse()
|
||||
@@ -337,38 +354,6 @@ namespace Jellyfin.MeiamSub.Thunder
|
||||
return value == SRT || value == ASS || value == SSA ? value : null;
|
||||
}
|
||||
|
||||
private static void ValidateSubtitleData(byte[] data, string format, string mediaType)
|
||||
{
|
||||
if (data == null || data.Length < 8)
|
||||
{
|
||||
throw new InvalidDataException("Subtitle response is empty.");
|
||||
}
|
||||
|
||||
if (data[0] == (byte)'P' && data[1] == (byte)'K' ||
|
||||
data.Length >= 7 && Encoding.ASCII.GetString(data, 0, 7) == "Rar!\u001a\u0007")
|
||||
{
|
||||
throw new InvalidDataException("Compressed subtitle responses are not supported by Thunder.");
|
||||
}
|
||||
|
||||
var sample = Encoding.UTF8.GetString(data, 0, Math.Min(data.Length, 4096)).TrimStart('\uFEFF', ' ', '\r', '\n', '\t');
|
||||
if (sample.StartsWith("<", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("{", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("[", StringComparison.Ordinal) ||
|
||||
mediaType?.IndexOf("html", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
mediaType?.IndexOf("json", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
throw new InvalidDataException("Thunder returned an error document instead of a subtitle.");
|
||||
}
|
||||
|
||||
var valid = format == SRT
|
||||
? sample.Contains("-->", StringComparison.Ordinal)
|
||||
: sample.Contains("[Script Info]", StringComparison.OrdinalIgnoreCase) || sample.Contains("Dialogue:", StringComparison.OrdinalIgnoreCase);
|
||||
if (!valid)
|
||||
{
|
||||
throw new InvalidDataException($"Downloaded content is not a valid {format} subtitle.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规范化语言代码
|
||||
/// </summary>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"guid": "E4CE9DA9-EF00-417C-96F2-861C512D45EB",
|
||||
"name": "MeiamSub.Thunder",
|
||||
"owner": "Meiam",
|
||||
"targetAbi": "10.11.0.0",
|
||||
"timestamp": "2026-08-07T07:11:29.0000000Z",
|
||||
"version": "1.0.16.0",
|
||||
"targetAbi": "10.10.7.0",
|
||||
"timestamp": "2026-09-16T08:13:02.0000000Z",
|
||||
"version": "1.0.17.0",
|
||||
"status": "Active",
|
||||
"imagePath": "thumb.png"
|
||||
}
|
||||
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.MeiamSub.Assrt", "
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.MeiamSub.Assrt", "Emby.MeiamSub.Assrt\Emby.MeiamSub.Assrt.csproj", "{D276A8D0-4B17-4EEF-94B6-61D646DE5A22}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeiamSubtitles.Tests", "tests\MeiamSubtitles.Tests\MeiamSubtitles.Tests.csproj", "{B8A8BFC1-8A0D-48C5-89AC-4DDDC2DDC3C9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -39,6 +41,10 @@ Global
|
||||
{D276A8D0-4B17-4EEF-94B6-61D646DE5A22}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D276A8D0-4B17-4EEF-94B6-61D646DE5A22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D276A8D0-4B17-4EEF-94B6-61D646DE5A22}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8A8BFC1-8A0D-48C5-89AC-4DDDC2DDC3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8A8BFC1-8A0D-48C5-89AC-4DDDC2DDC3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8A8BFC1-8A0D-48C5-89AC-4DDDC2DDC3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8A8BFC1-8A0D-48C5-89AC-4DDDC2DDC3C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
200
README.md
200
README.md
@@ -1,109 +1,165 @@
|
||||
# 🎬 MeiamSubtitles
|
||||
# MeiamSubtitles
|
||||
|
||||
**MeiamSubtitles** 是一款专为 **Emby** 和 **Jellyfin** 媒体服务器打造的中文字幕下载插件。它集成了 **迅雷影音** 与 **射手网** 的强大搜索能力,支持精准的视频哈希(Hash)匹配,让您的媒体库自动补全高质量字幕。
|
||||
|
||||
---
|
||||
**MeiamSubtitles** 是一款专为 **Emby** 和 **Jellyfin** 媒体服务器打造的中文字幕下载插件。它集成了 **[Assrt(伪射手网)](https://assrt.net/)** 与 **迅雷影音** 的字幕搜索能力,支持影片名称与视频特征(CID)匹配,帮助您的媒体库便捷地搜索和补全中文字幕。
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/91270/MeiamSubtitles/releases/latest"><img src="https://img.shields.io/github/v/release/91270/MeiamSubtitles" alt="Release"></a>
|
||||
<img src="https://img.shields.io/badge/.NET-Standard%202.1%20%7C%209.0-blueviolet.svg" alt=".NET Status">
|
||||
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20Win%20%7C%20OSX-brightgreen.svg" alt="Platform">
|
||||
<img src="https://img.shields.io/badge/license-Apache%202-blue" alt="LICENSE">
|
||||
<a href="https://github.com/91270/Emby.MeiamSub"><img src="https://img.shields.io/github/stars/91270/Emby.MeiamSub?label=Star%20this%20repo" alt="Star"></a>
|
||||
<a href="#插件与兼容性"><img src="https://img.shields.io/badge/Platform-Linux%20%7C%20Windows%20%7C%20macOS-brightgreen.svg" alt="Platform"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
|
||||
<a href="https://www.592.la/"><img src="https://img.shields.io/badge/博客-Meiam's%20Home-brightgreen.svg" alt="博客"></a>
|
||||
</p>
|
||||
|
||||
当前维护两个字幕源:
|
||||
|
||||
- **[Assrt(伪射手网)](https://assrt.net/)**:按影片名称搜索,支持下载并解析 ZIP、RAR 等压缩字幕;需要配置 Assrt API Token。
|
||||
- **Thunder**:根据视频文件计算 CID,优先匹配对应文件的字幕;无需额外配置。
|
||||
|
||||
> Shooter 的旧版接口已很少返回有效字幕,自 v1.0.16.0 起停止编译、维护和发布。
|
||||
|
||||
## 📣 广告时间
|
||||
|
||||
> **搬瓦工 $99 年付**:建站神器重出江湖,THE PLAN V1 传家宝套餐,18机房随意切换。
|
||||
> **循环优惠码**:`NODESEEK2026` (6.77%)
|
||||
> **[直达通道 (传家宝套餐)](https://bwh88.net/aff.php?aff=117&pid=87)**
|
||||
> **搬瓦工 $99 年付**:建站神器重出江湖,THE PLAN V1 传家宝套餐,18 机房随意切换。
|
||||
>
|
||||
> **配置**:2 核 / 2G / 40G SSD / 1TB 流量 / 2.5Gbps,支持 CN2 GIA 等大陆优化线路。
|
||||
>
|
||||
> **当前推荐循环优惠码**:`BWHNCXNVXV`(最高约 6.81%,实际优惠以结账页验证结果为准)
|
||||
>
|
||||
> **[直达通道(传家宝套餐)](https://bwh88.net/aff.php?aff=117&pid=87)**
|
||||
|
||||
## ✨ 核心特性
|
||||
## 功能
|
||||
|
||||
- **🚀 精准匹配**: 支持迅雷看看 (CID) 和射手网 (Hash) 双重校验逻辑,确保字幕与视频内容完美同步。
|
||||
- **⚡ 极致性能**: 核心采样算法全面采用**异步 I/O (Async/Await)** 模式,在大规模媒体库扫描时不会阻塞服务器线程。
|
||||
- **🌐 广泛兼容**: 深度适配 **Jellyfin 10.11+** 及 **Emby v4.9+**,支持 `zho`、`chi` 等多种国际化语言代码映射。
|
||||
- **🛡️ 稳定可靠**: 针对射手网 API 的老化问题增加了防御性校验,能有效处理乱码返回,保证系统长效稳定。
|
||||
- **📝 详尽日志**: 记录哈希计算耗时与接口原始响应,让问题排查不再是黑盒。
|
||||
- 同时支持 Emby 和 Jellyfin。
|
||||
- 支持 `zho`、`chi`、`zh-CN` 等中文语言代码。
|
||||
- 使用异步文件读取计算视频特征,适用于本地及网络媒体库。
|
||||
- 对搜索结果进行格式校验、去重和排序。
|
||||
- 下载前校验响应内容,避免将空响应或错误页面保存为字幕。
|
||||
- Assrt 支持从压缩包中选择并提取 `.srt`、`.ass`、`.ssa` 字幕。
|
||||
- 日志隐藏 Assrt Token,且不记录完整接口响应。
|
||||
|
||||
## 📦 项目组件说明
|
||||
## 插件与兼容性
|
||||
|
||||
| 组件名称 | 适用平台 | 目标框架 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Emby.MeiamSub.Thunder** | Emby | .NET Standard 2.1 | 迅雷看看字幕插件 |
|
||||
| **Emby.MeiamSub.Assrt** | Emby | .NET Standard 2.1 | Assrt 字幕插件 |
|
||||
| **Jellyfin.MeiamSub.Thunder** | Jellyfin | .NET 9.0 | 迅雷看看字幕插件 (现代 DI 架构) |
|
||||
| **Jellyfin.MeiamSub.Assrt** | Jellyfin | .NET 9.0 | Assrt 字幕插件 |
|
||||
| **Emby.MeiamSub.DevTool** | 开发调试 | .NET 8.0 | 哈希算法测试与 API 模拟工具 |
|
||||
| 插件 | 平台 | 目标框架 | 字幕源 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Emby.MeiamSub.Assrt` | Emby(版本限制见下文) | .NET Standard 2.1 | [Assrt.net](https://assrt.net/) |
|
||||
| `Emby.MeiamSub.Thunder` | Emby(版本限制见下文) | .NET Standard 2.1 | Thunder XMP |
|
||||
| `Jellyfin.MeiamSub.Assrt` | Jellyfin(版本限制见下文) | .NET 8.0 | [Assrt.net](https://assrt.net/) |
|
||||
| `Jellyfin.MeiamSub.Thunder` | Jellyfin(版本限制见下文) | .NET 8.0 | Thunder XMP |
|
||||
|
||||
> **插件状态:** 当前仅发布 Assrt 和 Thunder。Shooter 的旧版哈希接口已很少返回有效字幕,自 v1.0.16.0 起停止编译和发布。
|
||||
| 平台 | 兼容版本 |
|
||||
| --- | --- |
|
||||
| Emby | 4.8.10.0 至 4.10.0.40 |
|
||||
| Jellyfin | 10.10.7 至 12.1 |
|
||||
|
||||
---
|
||||
## 安装
|
||||
|
||||
## 🚀 快速安装
|
||||
### Jellyfin 存储库
|
||||
|
||||
### 第一步:获取插件
|
||||
前往 [GitHub Releases](https://github.com/91270/Emby.MeiamSub/releases) 下载最新版本的发布包。
|
||||
推荐通过插件存储库安装,以便接收后续更新。
|
||||
|
||||
> **🔔 推荐建议**:在媒体库设置中**不勾选**本插件作为默认自动下载器。建议仅在手动“搜索字幕”时使用,以获得更精准的人工筛选体验。
|
||||
1. 打开 Jellyfin 控制台的“插件 → 存储库”。
|
||||
2. 添加存储库名称 `MeiamSub`。
|
||||
3. 填写以下地址:
|
||||
|
||||
### 第二步:部署插件
|
||||
```text
|
||||
https://github.com/91270/MeiamSubtitles.Release/raw/main/Plugin/manifest-stable.json
|
||||
```
|
||||
|
||||
#### 🔹 方式 A:Jellyfin 存储库安装 (强烈推荐)
|
||||
Jellyfin 用户可直接添加官方存储库,实现一键安装与自动更新:
|
||||
1. 控制台 -> **插件** -> **存储库** -> 点击“添加”。
|
||||
2. 输入名称 `MeiamSub` 和 URL:
|
||||
`https://github.com/91270/MeiamSubtitles.Release/raw/main/Plugin/manifest-stable.json`
|
||||
3. 在“目录”中找到插件并安装,重启服务即可。
|
||||
4. 在“目录”中分别安装 `MeiamSub.Assrt` 或 `MeiamSub.Thunder`。
|
||||
5. 重启 Jellyfin。
|
||||
|
||||
#### 🔹 方式 B:手动安装 (Emby/通用)
|
||||
将下载的 `.dll` 文件(Jellyfin 用户请下载 `.zip` 并解压完整目录)放入服务器的 `plugins` 文件夹:
|
||||
### 手动安装
|
||||
|
||||
- **Windows**: `AppData\Local\jellyfin\plugins` 或 `Emby-Server\programdata\plugins`
|
||||
- **Linux/Docker**: `/config/plugins` 或 `/var/lib/emby/plugins`
|
||||
- **群晖/威联通**: 对应套件安装目录下的 `plugins` 文件夹
|
||||
从 [Releases](https://github.com/91270/MeiamSubtitles/releases/latest) 下载与平台对应的压缩包。
|
||||
|
||||
---
|
||||
#### Emby
|
||||
|
||||
## ❓ 常见问题排查 (FAQ)
|
||||
1. 解压 `Emby_v*.zip`。
|
||||
2. 将需要的插件 DLL 放入 Emby 的 `plugins` 目录。
|
||||
3. 不要额外复制 `SharpCompress.dll`;Assrt 复用 Emby 自带版本。
|
||||
4. 重启 Emby。
|
||||
|
||||
<details>
|
||||
<summary><b>1. 为什么在 Jellyfin 10.11+ 中搜不到字幕?</b></summary>
|
||||
新版 Jellyfin 采用了三位字母的语言代码(如 <code>zho</code>)。请确保您已升级至本插件的 <b>v1.0.13.0</b> 或更高版本,该版本已完美解决语言映射兼容性。
|
||||
</details>
|
||||
#### Jellyfin
|
||||
|
||||
<details>
|
||||
<summary><b>2. 为什么射手网有时候返回结果为空?</b></summary>
|
||||
由于射手网 API 维护状态不佳,对于部分冷门资源或 Hash 不匹配的文件,API 可能会返回非法数据。插件目前已增加防御逻辑,会自动忽略这些无效返回以保护服务器稳定。
|
||||
</details>
|
||||
1. 解压 `Jellyfin_v*.zip`。
|
||||
2. 将需要的整个插件目录复制到 Jellyfin 的 `plugins` 目录。
|
||||
3. Assrt 目录中的 `SharpCompress.dll` 是 Jellyfin 所需依赖,请勿单独删除。
|
||||
4. 重启 Jellyfin。
|
||||
|
||||
<details>
|
||||
<summary><b>3. 安装本插件后会影响 Open Subtitles 吗?</b></summary>
|
||||
不会。本插件已将优先级 (Order) 调整为 100(低优先级),并在代码层面优化了并发逻辑,确保官方插件能优先获取请求机会。
|
||||
</details>
|
||||
### 从旧版本升级
|
||||
|
||||
<details>
|
||||
<summary><b>4. 如何提供有效的错误反馈?</b></summary>
|
||||
如果确定有字幕但搜不到,请在 Issue 中提供日志里的 <code>Target</code> 文件名、计算出的 <code>FileHash</code> 以及 <code>ResponseBody</code> 内容。
|
||||
</details>
|
||||
- 升级到 v1.0.16.0 或更高版本后,删除旧的 Shooter DLL 和插件目录;Shooter 已停止维护和发布。
|
||||
- 同一个插件不要同时保留多个版本,否则服务器可能重复加载或启动失败。
|
||||
- Emby Assrt 只需保留插件 DLL,不要额外复制 `SharpCompress.dll`。
|
||||
- Jellyfin Assrt 必须保留发布包中同目录的 `SharpCompress.dll`。
|
||||
|
||||
---
|
||||
常见插件目录:
|
||||
|
||||
## 🤝 贡献与感谢
|
||||
| 环境 | 目录示例 |
|
||||
| --- | --- |
|
||||
| Jellyfin Windows | `%LOCALAPPDATA%\jellyfin\plugins` |
|
||||
| Jellyfin Linux | `/var/lib/jellyfin/plugins` |
|
||||
| Emby Windows | `%APPDATA%\Emby-Server\programdata\plugins` |
|
||||
| Docker | 容器映射的 `/config/plugins` |
|
||||
|
||||
欢迎通过提交 Issue 或 Pull Request 来完善本项目。
|
||||
## 配置
|
||||
|
||||
- **开发守则**: 遵循异步命名规范,详情请参阅 [.docs/开发协议.md](.docs/开发协议.md)。
|
||||
- **任务进展**: 查看 [.docs/任务清单.md](.docs/任务清单.md) 了解当前计划。
|
||||
- **致谢**: 感谢 [Emby.Subtitle.Subscene](https://github.com/nRafinia/Emby.Subtitle.Subscene) 提供的灵感与参考。
|
||||
### Assrt
|
||||
|
||||
---
|
||||
1. 从 [Assrt](https://assrt.net/) 获取 API Token。
|
||||
2. 打开插件配置页,将 Token 填入 `Assrt API Token`。
|
||||
3. 在媒体库的字幕下载器中启用 `MeiamSub.Assrt`。
|
||||
|
||||
## ⭐️ 给个星星
|
||||
未配置 Token 时,Assrt 会跳过搜索并在日志中给出提示。
|
||||
|
||||
如果你喜欢这个项目,请给一个 **Star**!这对我非常重要。
|
||||
### Thunder
|
||||
|
||||
如果你有稳定的 Google Drive 团队盘资源可供媒体库插件测试,欢迎联系:`91270#QQ.COM`
|
||||
Thunder 默认使用视频文件计算 CID。Jellyfin 版本还可在插件设置中启用元数据搜索,用影片名称辅助匹配。
|
||||
|
||||
---
|
||||
*Powered by Meiam*
|
||||
## 自动下载说明
|
||||
|
||||
建议先通过“搜索字幕”手动确认匹配效果,再决定是否启用媒体库自动下载。
|
||||
|
||||
- 同一媒体库的字幕下载语言只应添加一次。例如只保留一个 `zho`,重复添加会让服务器发起多次下载请求。
|
||||
- Emby 与 Jellyfin 如果共用媒体目录,会采用不同的字幕文件命名方式,因此目录中可能同时出现 `.zh-CN.*` 和 `.zho.*` 文件。
|
||||
- `SearchAllProviders` 表示同时搜索多个字幕源,不代表下载全部搜索结果。
|
||||
- 自动下载行为由 Emby/Jellyfin 调度;插件只响应服务器发出的搜索与下载请求。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 搜不到字幕
|
||||
|
||||
- 确认字幕语言已选择中文,并使用最新版插件。
|
||||
- Assrt 用户请确认 Token 已填写且有效。
|
||||
- Thunder 依赖视频文件内容计算 CID,请确认服务器能够读取媒体文件。
|
||||
- 某些影片或版本可能没有对应资源,可尝试启用元数据搜索或手动选择其他结果。
|
||||
|
||||
### 自动生成了多个字幕文件
|
||||
|
||||
首先检查媒体库的“字幕下载语言”是否存在重复项。服务器会为每个语言项分别执行一次下载,产生 `.0`、`.1` 等后缀。
|
||||
|
||||
手动连续下载不同的搜索结果时,服务器也会为避免覆盖已有字幕而添加数字后缀。这属于 Emby/Jellyfin 的文件保存行为,不表示插件正在循环下载。
|
||||
|
||||
### Assrt 提示找不到 SharpCompress
|
||||
|
||||
- Emby:仅安装插件 DLL,不要携带其他版本的 `SharpCompress.dll`。
|
||||
- Jellyfin:确保 Assrt 插件目录中包含发布包附带的 `SharpCompress.dll`。
|
||||
|
||||
### 如何反馈问题
|
||||
|
||||
请在 [Issues](https://github.com/91270/MeiamSubtitles/issues) 中提供:
|
||||
|
||||
- Emby/Jellyfin 版本与操作系统;
|
||||
- 插件名称和版本;
|
||||
- 媒体文件名、容器格式及问题复现步骤;
|
||||
- 对应时间段的服务器日志。
|
||||
|
||||
提交日志前请删除 Token、媒体路径和其他隐私信息。
|
||||
|
||||
## 支持项目
|
||||
|
||||
欢迎提交 Issue、Pull Request,或者为项目点一个 Star。
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
||||
|
||||
105
Shared/SubtitleContentValidator.cs
Normal file
105
Shared/SubtitleContentValidator.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace MeiamSubtitles.Shared
|
||||
{
|
||||
internal static class SubtitleContentValidator
|
||||
{
|
||||
public static void Validate(byte[] data, string format, string mediaType)
|
||||
{
|
||||
if (data == null || data.Length < 8)
|
||||
{
|
||||
throw new InvalidDataException("Subtitle response is empty.");
|
||||
}
|
||||
|
||||
if ((data[0] == (byte)'P' && data[1] == (byte)'K') ||
|
||||
(data.Length >= 7 && Encoding.ASCII.GetString(data, 0, 7) == "Rar!\u001a\u0007"))
|
||||
{
|
||||
throw new InvalidDataException("Compressed subtitle responses are not supported by Thunder.");
|
||||
}
|
||||
|
||||
if (mediaType?.IndexOf("html", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
mediaType?.IndexOf("json", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
throw new InvalidDataException("Thunder returned an error document instead of a subtitle.");
|
||||
}
|
||||
|
||||
var errorDocument = false;
|
||||
foreach (var sample in DecodeSamples(data))
|
||||
{
|
||||
if (sample.StartsWith("<", StringComparison.Ordinal) ||
|
||||
sample.StartsWith("{", StringComparison.Ordinal))
|
||||
{
|
||||
errorDocument = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsValidSubtitle(sample, format))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sample.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
errorDocument = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorDocument)
|
||||
{
|
||||
throw new InvalidDataException("Thunder returned an error document instead of a subtitle.");
|
||||
}
|
||||
|
||||
throw new InvalidDataException($"Downloaded content is not a valid {format} subtitle.");
|
||||
}
|
||||
|
||||
private static bool IsValidSubtitle(string sample, string format)
|
||||
{
|
||||
return format == "srt"
|
||||
? sample.IndexOf("-->", StringComparison.Ordinal) >= 0
|
||||
: sample.IndexOf("[Script Info]", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
sample.IndexOf("Dialogue:", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> DecodeSamples(byte[] data)
|
||||
{
|
||||
var length = Math.Min(data.Length, 4096);
|
||||
|
||||
if (data.Length >= 2)
|
||||
{
|
||||
if (data[0] == 0xFF && data[1] == 0xFE)
|
||||
{
|
||||
yield return DecodeSample(data, length, Encoding.Unicode);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (data[0] == 0xFE && data[1] == 0xFF)
|
||||
{
|
||||
yield return DecodeSample(data, length, Encoding.BigEndianUnicode);
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
|
||||
{
|
||||
yield return DecodeSample(data, length, Encoding.UTF8);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Thunder may return UTF-16 subtitles without a BOM. Try the common
|
||||
// single-byte/UTF-8 representation and both UTF-16 byte orders, then
|
||||
// accept only a candidate that contains the expected subtitle syntax.
|
||||
yield return DecodeSample(data, length, new UTF8Encoding(false, false));
|
||||
yield return DecodeSample(data, length, Encoding.Unicode);
|
||||
yield return DecodeSample(data, length, Encoding.BigEndianUnicode);
|
||||
}
|
||||
|
||||
private static string DecodeSample(byte[] data, int length, Encoding encoding)
|
||||
{
|
||||
return encoding.GetString(data, 0, length)
|
||||
.TrimStart('\uFEFF', ' ', '\r', '\n', '\t');
|
||||
}
|
||||
}
|
||||
}
|
||||
10
publish.ps1
10
publish.ps1
@@ -16,6 +16,8 @@ param (
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$SupportedEmbySdkVersion = "4.8.10"
|
||||
$SupportedJellyfinSdkVersion = "10.10.7"
|
||||
|
||||
Write-Host "=== Start Release Process, Target Version: $Version ===" -ForegroundColor Cyan
|
||||
|
||||
@@ -32,12 +34,20 @@ $EmbyThunderCsproj = "Emby.MeiamSub.Thunder\Emby.MeiamSub.Thunder.csproj"
|
||||
$EmbyAssrtCsproj = "Emby.MeiamSub.Assrt\Emby.MeiamSub.Assrt.csproj"
|
||||
|
||||
# Update NuGet package version if specified
|
||||
if ($JellyfinVersion -and $JellyfinVersion -ne $SupportedJellyfinSdkVersion) {
|
||||
throw "Jellyfin SDK must remain at $SupportedJellyfinSdkVersion for the verified 10.10.7-12.1 compatibility range. Update the compatibility matrix before changing it."
|
||||
}
|
||||
|
||||
if ($JellyfinVersion) {
|
||||
Write-Host "Upgrading Jellyfin.Controller NuGet dependency to $JellyfinVersion..." -ForegroundColor Yellow
|
||||
(Get-Content $ThunderCsproj) -replace '<PackageReference Include="Jellyfin.Controller" Version="[^"]+" />', "<PackageReference Include=`"Jellyfin.Controller`" Version=`"$JellyfinVersion`" />" | Set-Content $ThunderCsproj
|
||||
(Get-Content $AssrtCsproj) -replace '<PackageReference Include="Jellyfin.Controller" Version="[^"]+" />', "<PackageReference Include=`"Jellyfin.Controller`" Version=`"$JellyfinVersion`" />" | Set-Content $AssrtCsproj
|
||||
}
|
||||
|
||||
if ($EmbyVersion -and $EmbyVersion -ne $SupportedEmbySdkVersion) {
|
||||
throw "Emby SDK must remain at $SupportedEmbySdkVersion for the verified 4.8.10.0-4.10.0.40 compatibility range. Update the compatibility matrix before changing it."
|
||||
}
|
||||
|
||||
if ($EmbyVersion) {
|
||||
Write-Host "Upgrading MediaBrowser NuGet dependencies to $EmbyVersion..." -ForegroundColor Yellow
|
||||
(Get-Content $EmbyThunderCsproj) -replace '<PackageReference Include="MediaBrowser.Common" Version="[^"]+" />', "<PackageReference Include=`"MediaBrowser.Common`" Version=`"$EmbyVersion`" />" `
|
||||
|
||||
21
tests/MeiamSubtitles.Tests/MeiamSubtitles.Tests.csproj
Normal file
21
tests/MeiamSubtitles.Tests/MeiamSubtitles.Tests.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\..\Shared\SubtitleContentValidator.cs" Link="Shared\SubtitleContentValidator.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
84
tests/MeiamSubtitles.Tests/SubtitleContentValidatorTests.cs
Normal file
84
tests/MeiamSubtitles.Tests/SubtitleContentValidatorTests.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using MeiamSubtitles.Shared;
|
||||
using Xunit;
|
||||
|
||||
namespace MeiamSubtitles.Tests;
|
||||
|
||||
public class SubtitleContentValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AcceptsAssWithScriptInfoHeader()
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes("[Script Info]\r\nScriptType: v4.00+\r\n[Events]\r\nDialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,Hello");
|
||||
|
||||
SubtitleContentValidator.Validate(data, "ass", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsUtf16LittleEndianSrtWithoutBom()
|
||||
{
|
||||
var data = Encoding.Unicode.GetBytes("1\r\n00:00:01,000 --> 00:00:02,000\r\nHello\r\n");
|
||||
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsUtf16LittleEndianSrtWithoutBomWithLongChineseCue()
|
||||
{
|
||||
var content = "1\r\n00:00:01,000 --> 00:00:20,000\r\n" + new string('中', 300) + "\r\n";
|
||||
var data = Encoding.Unicode.GetBytes(content);
|
||||
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void AcceptsUtf16SrtInEitherByteOrder(bool bigEndian, bool withBom)
|
||||
{
|
||||
var encoding = new UnicodeEncoding(bigEndian, withBom);
|
||||
var content = "1\r\n00:00:01,000 --> 00:00:02,000\r\n你好\r\n";
|
||||
var body = encoding.GetBytes(content);
|
||||
var data = withBom ? encoding.GetPreamble().Concat(body).ToArray() : body;
|
||||
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<html><body>temporary error</body></html>", "application/octet-stream")]
|
||||
[InlineData("{\"error\":\"temporary error\"}", "application/json")]
|
||||
[InlineData("[\"temporary error\"]", "application/octet-stream")]
|
||||
public void RejectsErrorDocuments(string content, string mediaType)
|
||||
{
|
||||
var exception = Assert.Throws<InvalidDataException>(() =>
|
||||
SubtitleContentValidator.Validate(Encoding.UTF8.GetBytes(content), "srt", mediaType));
|
||||
|
||||
Assert.Contains("error document", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsCompressedSubtitleResponse()
|
||||
{
|
||||
var data = new byte[] { (byte)'P', (byte)'K', 3, 4, 5, 6, 7, 8 };
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() =>
|
||||
SubtitleContentValidator.Validate(data, "srt", "application/octet-stream"));
|
||||
|
||||
Assert.Contains("Compressed", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<html><!-- service unavailable --><body>Error</body></html>", "srt")]
|
||||
[InlineData("{\"error\":\"download --> failed\"}", "srt")]
|
||||
[InlineData("<html><body>Dialogue: download failed</body></html>", "ass")]
|
||||
public void RejectsErrorDocumentsThatContainSubtitleMarkers(string content, string format)
|
||||
{
|
||||
var exception = Assert.Throws<InvalidDataException>(() =>
|
||||
SubtitleContentValidator.Validate(Encoding.UTF8.GetBytes(content), format, "application/octet-stream"));
|
||||
|
||||
Assert.Contains("error document", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user