14 Commits

Author SHA1 Message Date
Meiam
74e576de93 程序逻辑优化 2022-04-10 14:21:09 +08:00
Meiam
bc72ca4ab0 降低依赖侵入,适配低版本 2022-04-09 16:07:26 +08:00
Meiam
2d4940156d 修改说明文件 2022-03-31 21:32:22 +08:00
Meiam
7da60776cb 更新说明文件 2022-03-31 21:20:14 +08:00
Meiam
03ce0a0dda 适配 Jellyfin 最新版本 2022-03-31 21:08:24 +08:00
Meiam
492294941f 降低依赖组件版本,支持旧版本 2021-12-23 21:43:44 +08:00
Meiam
18e00d72e2 更新说明文档 2021-12-23 20:49:04 +08:00
Meiam
ea887747b7 修复射手下载字幕提示 SSL 错误问题 2021-12-23 20:35:23 +08:00
Meiam
08a1936bf5 Update README.MD 2021-11-23 10:59:46 +08:00
Meiam
5d4b42c1f8 更新说明 2021-08-18 16:16:00 +08:00
Meiam
2216cb40b3 修改说明文件 2021-07-10 14:34:59 +08:00
Meiam
08c0071910 增加请求超时时间 2021-07-10 14:22:30 +08:00
Meiam
309da9469b 修复射手字幕插件为获取到正常返回异常报错问题 2021-07-01 17:45:37 +08:00
Meiam
69f15709ac 更新说明及项目文件 2021-07-01 16:05:57 +08:00
20 changed files with 931 additions and 53 deletions

View File

@@ -9,7 +9,7 @@ namespace Emby.Subtitle.DevTool
{
static void Main(string[] args)
{
//Console.WriteLine(ComputeFileHash($"X:\\Download\\喋血战士 (2020)\\喋血战士 (2020) 1080p AAC.mp4"));
//Console.WriteLine(ComputeFileHash($"X:\\Favorites\\Movie\\八佰 (2020)\\八佰 (2020) 1080p TrueHD.mkv"));
Console.WriteLine(ComputeFileHash($"D:\\Documents\\Downloads\\testidx.avi"));
Console.ReadKey();
}

View File

@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
<FileVersion>1.0.1.0</FileVersion>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.6.0</AssemblyVersion>
<FileVersion>1.0.6.0</FileVersion>
<Version>1.0.6</Version>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
@@ -10,6 +10,9 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<WarningLevel>2</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\Release</OutputPath>
</PropertyGroup>
<ItemGroup>
<None Remove="Thumb.png" />
</ItemGroup>
@@ -17,7 +20,7 @@
<EmbeddedResource Include="Thumb.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediaBrowser.Common" Version="4.6.0.50" />
<PackageReference Include="MediaBrowser.Server.Core" Version="4.6.0.50" />
<PackageReference Include="MediaBrowser.Common" Version="4.4.2" />
<PackageReference Include="MediaBrowser.Server.Core" Version="4.4.2" />
</ItemGroup>
</Project>

View File

@@ -47,6 +47,7 @@ namespace Emby.MeiamSub.Shooter
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClient = httpClient;
_logger.Info($"{Name} Init");
}
#endregion
@@ -60,7 +61,7 @@ namespace Emby.MeiamSub.Shooter
/// <returns></returns>
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
{
_logger.Debug($"MeiamSub.Shooter Search | Request -> { _jsonSerializer.SerializeToString(request) }");
_logger.Info($"{Name} Search | SubtitleSearchRequest -> { _jsonSerializer.SerializeToString(request) }");
var subtitles = await SearchSubtitlesAsync(request);
@@ -85,8 +86,9 @@ namespace Emby.MeiamSub.Shooter
HttpRequestOptions options = new HttpRequestOptions
{
Url = $"http://shooter.cn/api/subapi.php",
UserAgent = "Emby.MeiamSub.Shooter",
Url = $"http://www.shooter.cn/api/subapi.php",
UserAgent = $"{Name}",
TimeoutMs = 30000,
AcceptHeader = "*/*",
};
@@ -98,19 +100,19 @@ namespace Emby.MeiamSub.Shooter
{ "lang",request.Language == "chi" ? "chn" : "eng"}
});
_logger.Debug($"MeiamSub.Shooter Search | Request -> { _jsonSerializer.SerializeToString(options) }");
_logger.Info($"{Name} Search | Request -> { _jsonSerializer.SerializeToString(options) }");
var response = await _httpClient.Post(options);
_logger.Debug($"MeiamSub.Shooter Search | Response -> { _jsonSerializer.SerializeToString(response) }");
_logger.Info($"{Name} Search | Response -> { _jsonSerializer.SerializeToString(response) }");
if (response.StatusCode == HttpStatusCode.OK)
if (response.StatusCode == HttpStatusCode.OK && response.ContentType.Contains("application/json"))
{
var subtitleResponse = _jsonSerializer.DeserializeFromStream<List<SubtitleResponseRoot>>(response.Content);
if (subtitleResponse != null)
{
_logger.Debug($"MeiamSub.Shooter Search | Response -> { _jsonSerializer.SerializeToString(subtitleResponse) }");
_logger.Info($"{Name} Search | Response -> { _jsonSerializer.SerializeToString(subtitleResponse) }");
var remoteSubtitleInfos = new List<RemoteSubtitleInfo>();
@@ -130,20 +132,20 @@ namespace Emby.MeiamSub.Shooter
})),
Name = $"[MEIAMSUB] { Path.GetFileName(request.MediaPath) } | {request.TwoLetterISOLanguageName} | 射手",
Author = "Meiam ",
ProviderName = "MeiamSub.Shooter",
ProviderName = $"{Name}",
Format = subFile.Ext,
Comment = $"Format : { ExtractFormat(subFile.Ext)}"
});
}
}
_logger.Debug($"MeiamSub.Shooter Search | Summary -> Get { remoteSubtitleInfos.Count } Subtitles");
_logger.Info($"{Name} Search | Summary -> Get { remoteSubtitleInfos.Count } Subtitles");
return remoteSubtitleInfos;
}
}
_logger.Debug($"MeiamSub.Shooter Search | Summary -> Get 0 Subtitles");
_logger.Info($"{Name} Search | Summary -> Get 0 Subtitles");
return Array.Empty<RemoteSubtitleInfo>();
}
@@ -158,10 +160,7 @@ namespace Emby.MeiamSub.Shooter
/// <returns></returns>
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
{
await Task.Run(() =>
{
_logger.Debug($"MeiamSub.Shooter DownloadSub | Request -> {id}");
});
_logger.Info($"{Name} DownloadSub | Request -> {id}");
return await DownloadSubAsync(id);
}
@@ -175,14 +174,25 @@ namespace Emby.MeiamSub.Shooter
{
var downloadSub = _jsonSerializer.DeserializeFromString<DownloadSubInfo>(Base64Decode(info));
_logger.Debug($"MeiamSub.Shooter DownloadSub | Url -> { downloadSub.Url } | Format -> { downloadSub.Format } | Language -> { downloadSub.Language } ");
if (downloadSub == null)
{
return new SubtitleResponse();
}
downloadSub.Url = downloadSub.Url.Replace("https://www.shooter.cn", "http://www.shooter.cn");
_logger.Info($"{Name} DownloadSub | Url -> { downloadSub.Url } | Format -> { downloadSub.Format } | Language -> { downloadSub.Language } ");
var response = await _httpClient.GetResponse(new HttpRequestOptions
{
Url = downloadSub.Url
Url = downloadSub.Url,
UserAgent = $"{Name}",
TimeoutMs = 30000,
AcceptHeader = "*/*",
});
_logger.Debug($"MeiamSub.Shooter DownloadSub | Response -> { response.StatusCode }");
_logger.Info($"{Name} DownloadSub | Response -> { response.StatusCode }");
if (response.StatusCode == HttpStatusCode.OK)
{

View File

@@ -1,14 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
<FileVersion>1.0.1.0</FileVersion>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.6.0</AssemblyVersion>
<FileVersion>1.0.6.0</FileVersion>
<Version>1.0.6</Version>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\Release</OutputPath>
</PropertyGroup>
<ItemGroup>
<None Remove="Thumb.png" />
@@ -19,8 +23,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediaBrowser.Common" Version="4.6.0.50" />
<PackageReference Include="MediaBrowser.Server.Core" Version="4.6.0.50" />
<PackageReference Include="MediaBrowser.Common" Version="4.4.2" />
<PackageReference Include="MediaBrowser.Server.Core" Version="4.4.2" />
</ItemGroup>
</Project>

View File

@@ -28,10 +28,6 @@ namespace Emby.MeiamSub.Thunder.Model
///
/// </summary>
public int svote { get; set; }
/// <summary>
///
/// </summary>
public int roffset { get; set; }
}
public class SubtitleResponseRoot

View File

@@ -47,6 +47,7 @@ namespace Emby.MeiamSub.Thunder
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClient = httpClient;
_logger.Info($"{Name} Init");
}
#endregion
@@ -60,7 +61,7 @@ namespace Emby.MeiamSub.Thunder
/// <returns></returns>
public async Task<IEnumerable<RemoteSubtitleInfo>> Search(SubtitleSearchRequest request, CancellationToken cancellationToken)
{
_logger.Debug($"MeiamSub.Thunder Search | Request -> { _jsonSerializer.SerializeToString(request) }");
_logger.Info($"{Name} Search | SubtitleSearchRequest -> { _jsonSerializer.SerializeToString(request) }");
var subtitles = await SearchSubtitlesAsync(request);
@@ -83,10 +84,14 @@ namespace Emby.MeiamSub.Thunder
var response = await _httpClient.GetResponse(new HttpRequestOptions
{
Url = $"http://sub.xmp.sandai.net:8000/subxl/{cid}.json"
//Url = $"http://sub.xmp.sandai.net:8000/subxl/{cid}.json",
Url = $"http://subtitle.kankan.xunlei.com:8000/subxl/{cid}.json",
UserAgent = $"{Name}",
TimeoutMs = 30000,
AcceptHeader = "*/*",
});
_logger.Debug($"MeiamSub.Thunder Search | Response -> { _jsonSerializer.SerializeToString(response) }");
_logger.Info($"{Name} Search | Response -> { _jsonSerializer.SerializeToString(response) }");
if (response.StatusCode == HttpStatusCode.OK)
{
@@ -94,13 +99,13 @@ namespace Emby.MeiamSub.Thunder
if (subtitleResponse != null)
{
_logger.Debug($"MeiamSub.Thunder Search | Response -> { _jsonSerializer.SerializeToString(subtitleResponse) }");
_logger.Info($"{Name} Search | Response -> { _jsonSerializer.SerializeToString(subtitleResponse) }");
var subtitles = subtitleResponse.sublist.Where(m => !string.IsNullOrEmpty(m.sname));
if (subtitles.Count() > 0)
{
_logger.Debug($"MeiamSub.Thunder Search | Summary -> Get { subtitles.Count() } Subtitles");
_logger.Info($"{Name} Search | Summary -> Get { subtitles.Count() } Subtitles");
return subtitles.Select(m => new RemoteSubtitleInfo()
{
@@ -115,7 +120,7 @@ namespace Emby.MeiamSub.Thunder
Name = $"[MEIAMSUB] { Path.GetFileName(request.MediaPath) } | {request.TwoLetterISOLanguageName} | 迅雷",
Author = "Meiam ",
CommunityRating = Convert.ToSingle(m.rate),
ProviderName = "MeiamSub.Thunder",
ProviderName = $"{Name}",
Format = ExtractFormat(m.sname),
Comment = $"Format : { ExtractFormat(m.sname)} - Rate : { m.rate }"
}).OrderByDescending(m => m.CommunityRating);
@@ -123,7 +128,7 @@ namespace Emby.MeiamSub.Thunder
}
}
_logger.Debug($"MeiamSub.Thunder Search | Summary -> Get 0 Subtitles");
_logger.Info($"{Name} Search | Summary -> Get 0 Subtitles");
return Array.Empty<RemoteSubtitleInfo>();
}
@@ -138,10 +143,7 @@ namespace Emby.MeiamSub.Thunder
/// <returns></returns>
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
{
await Task.Run(() =>
{
_logger.Debug($"MeiamSub.Thunder DownloadSub | Request -> {id}");
});
_logger.Info($"{Name} DownloadSub | Request -> {id}");
return await DownloadSubAsync(id);
}
@@ -155,14 +157,22 @@ namespace Emby.MeiamSub.Thunder
{
var downloadSub = _jsonSerializer.DeserializeFromString<DownloadSubInfo>(Base64Decode(info));
_logger.Debug($"MeiamSub.Thunder DownloadSub | Url -> { downloadSub.Url } | Format -> { downloadSub.Format } | Language -> { downloadSub.Language } ");
if (downloadSub == null)
{
return new SubtitleResponse();
}
_logger.Info($"{Name} DownloadSub | Url -> { downloadSub.Url } | Format -> { downloadSub.Format } | Language -> { downloadSub.Language } ");
var response = await _httpClient.GetResponse(new HttpRequestOptions
{
Url = downloadSub.Url
Url = downloadSub.Url,
UserAgent = $"{Name}",
TimeoutMs = 30000,
AcceptHeader = "*/*",
});
_logger.Debug($"MeiamSub.Thunder DownloadSub | Response -> { response.StatusCode }");
_logger.Info($"{Name} DownloadSub | Response -> { response.StatusCode }");
if (response.StatusCode == HttpStatusCode.OK)
{

View File

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

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>1.0.6</Version>
<AssemblyVersion>1.0.6.0</AssemblyVersion>
<FileVersion>1.0.6.0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\Release</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.7.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Jellyfin.MeiamSub.Shooter.Model
{
public class DownloadSubInfo
{
public string Url { get; set; }
public string Format { get; set; }
public string Language { get; set; }
public string TwoLetterISOLanguageName { get; set; }
public bool? IsForced { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Jellyfin.MeiamSub.Shooter.Model
{
public class SubtitleResponseRoot
{
public string Desc { get; set; }
public int Delay { get; set; }
public SubFileInfo[] Files { get; set; }
}
public class SubFileInfo
{
public string Ext { get; set; }
public string Link { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using Jellyfin.MeiamSub.Shooter.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Serialization;
using System;
using System.IO;
namespace Jellyfin.MeiamSub.Shooter
{
/// <summary>
/// 插件入口
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>
{
/// <summary>
/// 插件ID
/// </summary>
public override Guid Id => new Guid("038D37A2-7A1E-4C01-9B6D-AA215D29AB4C");
/// <summary>
/// 插件名称
/// </summary>
public override string Name => "MeiamSub.Shooter";
/// <summary>
/// 插件描述
/// </summary>
public override string Description => "Download subtitles from Shooter";
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
public static Plugin Instance { get; private set; }
}
}

View File

@@ -0,0 +1,320 @@
using Jellyfin.MeiamSub.Shooter.Model;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
namespace Jellyfin.MeiamSub.Shooter
{
/// <summary>
/// 迅雷字幕组件
/// </summary>
public class ShooterProvider : ISubtitleProvider, IHasOrder
{
#region
public const string ASS = "ass";
public const string SSA = "ssa";
public const string SRT = "srt";
private readonly ILogger<ShooterProvider> _logger;
private static readonly HttpClient _httpClient = new HttpClient();
public int Order => 0;
public string Name => "MeiamSub.Shooter";
/// <summary>
/// 支持电影、剧集
/// </summary>
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
#endregion
#region
public ShooterProvider(ILogger<ShooterProvider> logger)
{
_logger = logger;
_httpClient.Timeout = TimeSpan.FromSeconds(30);
_logger.LogInformation($"{Name} Init");
}
#endregion
#region
/// <summary>
/// 查询请求
/// </summary>
/// <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) }");
var subtitles = await SearchSubtitlesAsync(request);
return subtitles;
}
/// <summary>
/// 查询字幕
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
{
if (request.Language != "chi" && request.Language != "eng")
{
return Array.Empty<RemoteSubtitleInfo>();
}
FileInfo fileInfo = new(request.MediaPath);
var hash = ComputeFileHash(fileInfo);
var content = new StringContent(JsonSerializer.Serialize(new
{
filehash = HttpUtility.UrlEncode(hash),
pathinfo = HttpUtility.UrlEncode(request.MediaPath),
format = "json",
lang = request.Language == "chi" ? "chn" : "eng"
}), Encoding.UTF8, "application/json");
var options = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri($"http://www.shooter.cn/api/subapi.php"),
Content = content,
Headers =
{
UserAgent = { new ProductInfoHeaderValue(new ProductHeaderValue($"{Name}")) },
Accept = { new MediaTypeWithQualityHeaderValue("*/*") }
}
};
_logger.LogInformation($"{Name} Search | Request -> { JsonSerializer.Serialize(options) }");
var response = await _httpClient.SendAsync(options);
_logger.LogInformation($"{Name} Search | Response -> { JsonSerializer.Serialize(response) }");
if (response.StatusCode == HttpStatusCode.OK && response.Headers.Any(m => m.Value.Contains("application/json")))
{
var subtitleResponse = JsonSerializer.Deserialize<List<SubtitleResponseRoot>>(await response.Content.ReadAsStringAsync());
if (subtitleResponse != null)
{
_logger.LogInformation($"{Name} Search | Response -> { JsonSerializer.Serialize(subtitleResponse) }");
var remoteSubtitleInfos = new List<RemoteSubtitleInfo>();
foreach (var subFileInfo in subtitleResponse)
{
foreach (var subFile in subFileInfo.Files)
{
remoteSubtitleInfos.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)}"
});
}
}
_logger.LogInformation($"{Name} Search | Summary -> Get { remoteSubtitleInfos.Count } Subtitles");
return remoteSubtitleInfos;
}
}
_logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles");
return Array.Empty<RemoteSubtitleInfo>();
}
#endregion
#region
/// <summary>
/// 下载请求
/// </summary>
/// <param name="id"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
{
_logger.LogInformation($"{Name} DownloadSub | Request -> {id}");
return await DownloadSubAsync(id);
}
/// <summary>
/// 下载字幕
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
private async Task<SubtitleResponse> DownloadSubAsync(string info)
{
var downloadSub = JsonSerializer.Deserialize<DownloadSubInfo>(Base64Decode(info));
if (downloadSub == null)
{
return new SubtitleResponse();
}
downloadSub.Url = downloadSub.Url.Replace("https://www.shooter.cn", "http://www.shooter.cn");
_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,
};
}
return new SubtitleResponse();
}
#endregion
#region
/// <summary>
/// Base64 加密
/// </summary>
/// <param name="plainText">明文</param>
/// <returns></returns>
public static string Base64Encode(string plainText)
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
/// <summary>
/// Base64 解密
/// </summary>
/// <param name="base64EncodedData"></param>
/// <returns></returns>
public static string Base64Decode(string base64EncodedData)
{
var base64EncodedBytes = Convert.FromBase64String(base64EncodedData);
return Encoding.UTF8.GetString(base64EncodedBytes);
}
/// <summary>
/// 提取格式化字幕类型
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
protected string ExtractFormat(string text)
{
string result = null;
if (text != null)
{
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 result;
}
/// <summary>
/// 获取文件 Hash (射手)
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static string ComputeFileHash(FileInfo fileInfo)
{
string ret = "";
if (!fileInfo.Exists || fileInfo.Length < 8 * 1024)
{
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)
{
fs.Seek(offset[i], 0);
fs.Read(bBuf, 0, 4 * 1024);
MD5 md5Hash = MD5.Create();
byte[] data = md5Hash.ComputeHash(bBuf);
StringBuilder sBuilder = new StringBuilder();
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
}
}

View File

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

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net5.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
<Version>1.0.6</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\Release</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.7.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Jellyfin.MeiamSub.Thunder.Model
{
public class DownloadSubInfo
{
public string Url { get; set; }
public string Format { get; set; }
public string Language { get; set; }
public string TwoLetterISOLanguageName { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
namespace Jellyfin.MeiamSub.Thunder.Model
{
public class SublistItem
{
/// <summary>
///
/// </summary>
public string scid { get; set; }
/// <summary>
///
/// </summary>
public string sname { get; set; }
/// <summary>
/// 未知语言
/// </summary>
public string language { get; set; }
/// <summary>
///
/// </summary>
public string rate { get; set; }
/// <summary>
///
/// </summary>
public string surl { get; set; }
/// <summary>
///
/// </summary>
public int svote { get; set; }
}
public class SubtitleResponseRoot
{
/// <summary>
///
/// </summary>
public List<SublistItem> sublist { get; set; }
}
}

View File

@@ -0,0 +1,39 @@
using Jellyfin.MeiamSub.Thunder.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Serialization;
using System;
namespace Jellyfin.MeiamSub.Thunder
{
/// <summary>
/// 插件入口
/// </summary>
public class Plugin : BasePlugin<PluginConfiguration>
{
/// <summary>
/// 插件ID
/// </summary>
public override Guid Id => new Guid("E4CE9DA9-EF00-417C-96F2-861C512D45EB");
/// <summary>
/// 插件名称
/// </summary>
public override string Name => "MeiamSub.Thunder";
/// <summary>
/// 插件描述
/// </summary>
public override string Description => "Download subtitles from Thunder XMP";
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
public static Plugin Instance { get; private set; }
}
}

View File

@@ -0,0 +1,282 @@
using Jellyfin.MeiamSub.Thunder.Model;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.MeiamSub.Thunder
{
/// <summary>
/// 迅雷字幕组件
/// </summary>
public class ThunderProvider : ISubtitleProvider, IHasOrder
{
#region
public const string ASS = "ass";
public const string SSA = "ssa";
public const string SRT = "srt";
private readonly ILogger<ThunderProvider> _logger;
private static readonly HttpClient _httpClient = new HttpClient();
public int Order => 0;
public string Name => "MeiamSub.Thunder";
/// <summary>
/// 支持电影、剧集
/// </summary>
public IEnumerable<VideoContentType> SupportedMediaTypes => new List<VideoContentType>() { VideoContentType.Movie, VideoContentType.Episode };
#endregion
#region
public ThunderProvider(ILogger<ThunderProvider> logger)
{
_logger = logger;
_httpClient.Timeout = TimeSpan.FromSeconds(30);
_logger.LogInformation($"{Name} Init");
}
#endregion
#region
/// <summary>
/// 查询请求
/// </summary>
/// <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) }");
var subtitles = await SearchSubtitlesAsync(request);
return subtitles;
}
/// <summary>
/// 查询字幕
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitlesAsync(SubtitleSearchRequest request)
{
if (request.Language != "chi")
{
return Array.Empty<RemoteSubtitleInfo>();
}
var cid = GetCidByFile(request.MediaPath);
using var options = new HttpRequestMessage
{
Method = HttpMethod.Get,
//RequestUri = new Uri($"http://sub.xmp.sandai.net:8000/subxl/{cid}.json"),
RequestUri = new Uri($"http://subtitle.kankan.xunlei.com:8000/subxl/{cid}.json"),
Headers =
{
UserAgent = { new ProductInfoHeaderValue(new ProductHeaderValue($"{Name}")) },
Accept = { new MediaTypeWithQualityHeaderValue("*/*") },
}
};
var response = await _httpClient.SendAsync(options);
_logger.LogInformation($"{Name} Search | Response -> { JsonSerializer.Serialize(response) }");
if (response.StatusCode == HttpStatusCode.OK)
{
var subtitleResponse = JsonSerializer.Deserialize<SubtitleResponseRoot>(await response.Content.ReadAsStringAsync());
if (subtitleResponse != null)
{
_logger.LogInformation($"{Name} Search | Response -> { JsonSerializer.Serialize(subtitleResponse) }");
var subtitles = subtitleResponse.sublist.Where(m => !string.IsNullOrEmpty(m.sname));
if (subtitles.Count() > 0)
{
_logger.LogInformation($"{Name} Search | Summary -> Get { subtitles.Count() } Subtitles");
return subtitles.Select(m => new RemoteSubtitleInfo()
{
Id = Base64Encode(JsonSerializer.Serialize(new DownloadSubInfo
{
Url = m.surl,
Format = ExtractFormat(m.sname),
Language = request.Language,
TwoLetterISOLanguageName = request.TwoLetterISOLanguageName,
})),
Name = $"[MEIAMSUB] { Path.GetFileName(request.MediaPath) } | {request.TwoLetterISOLanguageName} | 迅雷",
Author = "Meiam ",
CommunityRating = Convert.ToSingle(m.rate),
ProviderName = $"{Name}",
Format = ExtractFormat(m.sname),
Comment = $"Format : { ExtractFormat(m.sname)} - Rate : { m.rate }"
}).OrderByDescending(m => m.CommunityRating);
}
}
}
_logger.LogInformation($"{Name} Search | Summary -> Get 0 Subtitles");
return Array.Empty<RemoteSubtitleInfo>();
}
#endregion
#region
/// <summary>
/// 下载请求
/// </summary>
/// <param name="id"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
{
_logger.LogInformation($"{Name} DownloadSub | Request -> {id}");
return await DownloadSubAsync(id);
}
/// <summary>
/// 下载字幕
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
private async Task<SubtitleResponse> DownloadSubAsync(string info)
{
var downloadSub = JsonSerializer.Deserialize<DownloadSubInfo>(Base64Decode(info));
if (downloadSub == null)
{
return new SubtitleResponse();
}
_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,
};
}
return new SubtitleResponse();
}
#endregion
#region
/// <summary>
/// Base64 加密
/// </summary>
/// <param name="plainText">明文</param>
/// <returns></returns>
public static string Base64Encode(string plainText)
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
/// <summary>
/// Base64 解密
/// </summary>
/// <param name="base64EncodedData"></param>
/// <returns></returns>
public static string Base64Decode(string base64EncodedData)
{
var base64EncodedBytes = Convert.FromBase64String(base64EncodedData);
return Encoding.UTF8.GetString(base64EncodedBytes);
}
/// <summary>
/// 提取格式化字幕类型
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
protected string ExtractFormat(string text)
{
string result = null;
if (text != null)
{
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 result;
}
/// <summary>
/// 获取文件 CID (迅雷)
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
private string GetCidByFile(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);
buffer = SHA1.ComputeHash(buffer, 0, 0xf000);
}
var result = "";
foreach (var i in buffer)
{
result += string.Format("{0:X2}", i);
}
return result;
}
#endregion
}
}

View File

@@ -7,7 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.MeiamSub.Thunder", "Em
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.MeiamSub.DevTool", "Emby.MeiamSub.DevTool\Emby.MeiamSub.DevTool.csproj", "{6B0C23EA-EC24-4FB0-948E-094E84AEBF21}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.MeiamSub.Shooter", "Emby.MeiamSub.Shooter\Emby.MeiamSub.Shooter.csproj", "{0F502AEB-0FF4-44FA-8391-13AD61FC5490}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.MeiamSub.Shooter", "Emby.MeiamSub.Shooter\Emby.MeiamSub.Shooter.csproj", "{0F502AEB-0FF4-44FA-8391-13AD61FC5490}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.MeiamSub.Thunder", "Jellyfin.MeiamSub.Thunder\Jellyfin.MeiamSub.Thunder.csproj", "{4676AA1B-CC6C-42DC-BD69-6A293BAE8823}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MeiamSub.Shooter", "Jellyfin.MeiamSub.Shooter\Jellyfin.MeiamSub.Shooter.csproj", "{8F77E155-9A91-4882-82E8-E8D69FECD246}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -27,6 +31,14 @@ Global
{0F502AEB-0FF4-44FA-8391-13AD61FC5490}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F502AEB-0FF4-44FA-8391-13AD61FC5490}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F502AEB-0FF4-44FA-8391-13AD61FC5490}.Release|Any CPU.Build.0 = Release|Any CPU
{4676AA1B-CC6C-42DC-BD69-6A293BAE8823}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4676AA1B-CC6C-42DC-BD69-6A293BAE8823}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4676AA1B-CC6C-42DC-BD69-6A293BAE8823}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4676AA1B-CC6C-42DC-BD69-6A293BAE8823}.Release|Any CPU.Build.0 = Release|Any CPU
{8F77E155-9A91-4882-82E8-E8D69FECD246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F77E155-9A91-4882-82E8-E8D69FECD246}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F77E155-9A91-4882-82E8-E8D69FECD246}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F77E155-9A91-4882-82E8-E8D69FECD246}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,5 +1,5 @@
# Emby.MeiamSub
Emby 中文字幕插件 ,支持 迅雷影音、射手网、 精准匹配,自动下载
# MeiamSubtitles
Emby Jellyfin 中文字幕插件 ,支持 迅雷影音、射手网、 精准匹配,自动下载
[![.NET CORE](https://img.shields.io/badge/.NET%20Core-3.1-d.svg)](#)
@@ -22,8 +22,8 @@ Emby 中文字幕插件 ,支持 迅雷影音、射手网、 精准匹配,自
## 功能介绍
- [x] 支持 迅雷看看 Hash 匹配自动下载字幕
- [ ] 支持 射手影音 Hash 匹配自动下载字幕
- [x] 支持 迅雷看看 字幕下载 Hash匹配
- [x] 支持 射手影音 字幕下载 Hash匹配
## 项目说明
@@ -31,13 +31,17 @@ Emby 中文字幕插件 ,支持 迅雷影音、射手网、 精准匹配,自
| # | 模块功能 | 项目文件 | 说明
|---|-------------------------------|-------------------------------|-------------------------------
| 1 | 开发程序 | Emby.MeiamSub.DevTool | 项目开发测试调试使用
| 2 | 字幕插件 | Emby.MeiamSub.Thunder | 迅雷看看字幕插件
| 3 | 字幕插件 | Emby.MeiamSub.Shooter | 射手影音字幕插件
| 2 | 字幕插件 | Emby.MeiamSub.Thunder | 迅雷看看字幕插件 - Emby
| 3 | 字幕插件 | Emby.MeiamSub.Shooter | 射手影音字幕插件 - Emby
| 3 | 字幕插件 | Jellyfin.MeiamSub.Shooter | 迅雷看看字幕插件 - Jellyfin
| 3 | 字幕插件 | Jellyfin.MeiamSub.Thunder | 射手影音字幕插件 - Jellyfin
## 使用插件
首先下载已编译好的插件 [LINK](https://github.com/91270/Emby.MeiamSub/releases)
### WINDOWS
```bash
复制插件文件到 Emby-Server\Programdata\Plugins\
@@ -54,6 +58,14 @@ Emby 中文字幕插件 ,支持 迅雷影音、射手网、 精准匹配,自
&nbsp;
### 群晖
```bash
复制插件文件到 /var/packages/EmbyServer/target/var/plugins
复制插件文件到 /var/packages/EmbyServer/target/system/plugins
重启服务
```
&nbsp;
## 贡献