From 66ed8ff49ce97fe35c3f980285ef4715a5d97b79 Mon Sep 17 00:00:00 2001 From: Meiam <91270@qq.com> Date: Mon, 15 Jun 2026 15:19:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20Assrt(=E4=BC=AA=E5=B0=84?= =?UTF-8?q?=E6=89=8B)=E5=AD=97=E5=B9=95=E6=8F=90=E4=BE=9B=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=EF=BC=8C=E6=94=AF=E6=8C=81=20Emby/Jellyfin=20?= =?UTF-8?q?=E5=8F=8C=E5=B9=B3=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.MeiamSub.Assrt/AssrtProvider.cs | 429 ++++++++++++++++++ .../Emby.MeiamSub.Assrt.csproj | 34 ++ Emby.MeiamSub.Assrt/Model/AssrtModels.cs | 57 +++ Emby.MeiamSub.Assrt/Plugin.cs | 92 ++++ Emby.MeiamSub.Assrt/Thumb.png | Bin 0 -> 17825 bytes .../Emby.MeiamSub.Shooter.csproj | 2 +- Emby.MeiamSub.Shooter/Plugin.cs | 2 +- .../Emby.MeiamSub.Thunder.csproj | 2 +- Jellyfin.MeiamSub.Assrt/AssrtProvider.cs | 409 +++++++++++++++++ .../Configuration/PluginConfiguration.cs | 26 ++ .../Configuration/configPage.html | 76 ++++ .../Jellyfin.MeiamSub.Assrt.csproj | 33 ++ Jellyfin.MeiamSub.Assrt/Model/AssrtModels.cs | 97 ++++ Jellyfin.MeiamSub.Assrt/Plugin.cs | 55 +++ .../PluginServiceRegistrator.cs | 26 ++ Jellyfin.MeiamSub.Assrt/meta.json | 12 + Jellyfin.MeiamSub.Assrt/thumb.png | Bin 0 -> 18337 bytes .../Jellyfin.MeiamSub.Shooter.csproj | 2 +- Jellyfin.MeiamSub.Shooter/Plugin.cs | 2 +- .../Jellyfin.MeiamSub.Thunder.csproj | 2 +- MeiamSubtitles.sln | 14 +- publish.ps1 | 27 ++ 22 files changed, 1392 insertions(+), 7 deletions(-) create mode 100644 Emby.MeiamSub.Assrt/AssrtProvider.cs create mode 100644 Emby.MeiamSub.Assrt/Emby.MeiamSub.Assrt.csproj create mode 100644 Emby.MeiamSub.Assrt/Model/AssrtModels.cs create mode 100644 Emby.MeiamSub.Assrt/Plugin.cs create mode 100644 Emby.MeiamSub.Assrt/Thumb.png create mode 100644 Jellyfin.MeiamSub.Assrt/AssrtProvider.cs create mode 100644 Jellyfin.MeiamSub.Assrt/Configuration/PluginConfiguration.cs create mode 100644 Jellyfin.MeiamSub.Assrt/Configuration/configPage.html create mode 100644 Jellyfin.MeiamSub.Assrt/Jellyfin.MeiamSub.Assrt.csproj create mode 100644 Jellyfin.MeiamSub.Assrt/Model/AssrtModels.cs create mode 100644 Jellyfin.MeiamSub.Assrt/Plugin.cs create mode 100644 Jellyfin.MeiamSub.Assrt/PluginServiceRegistrator.cs create mode 100644 Jellyfin.MeiamSub.Assrt/meta.json create mode 100644 Jellyfin.MeiamSub.Assrt/thumb.png diff --git a/Emby.MeiamSub.Assrt/AssrtProvider.cs b/Emby.MeiamSub.Assrt/AssrtProvider.cs new file mode 100644 index 0000000..4613015 --- /dev/null +++ b/Emby.MeiamSub.Assrt/AssrtProvider.cs @@ -0,0 +1,429 @@ +using Emby.MeiamSub.Assrt.Model; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +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 System.Net.Http; + +namespace Emby.MeiamSub.Assrt +{ + /// + /// Assrt (伪射手) 字幕提供程序 (Emby 适配版) + /// + public class AssrtProvider : ISubtitleProvider, IHasOrder + { + private const string ApiBaseUrl = "https://api.assrt.net/v1"; + + public const string ASS = "ass"; + public const string SSA = "ssa"; + public const string SRT = "srt"; + + protected readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + + public int Order => 90; // 优先级高于老旧源 + + public string Name => "MeiamSub.Assrt"; + + public IEnumerable SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode }; + + public AssrtProvider(ILogManager logManager, IJsonSerializer jsonSerializer, IHttpClient httpClient) + { + _logger = logManager.GetLogger(GetType().Name); + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + _logger.Info($"{Name} Init"); + } + + public async Task> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) + { + _logger.Info($"{Name} Search | Received request for: {(request?.MediaPath ?? "NULL")}"); + + try + { + if (request == null) + { + return Array.Empty(); + } + + var config = Plugin.Instance?.Options; + if (config == null || string.IsNullOrWhiteSpace(config.AssrtApiToken)) + { + _logger.Warn($"{Name} Search | AssrtApiToken is empty, skip search. Please configure the token in plugin settings."); + return Array.Empty(); + } + + var language = NormalizeLanguage(request.Language); + if (language != "chi") + { + _logger.Info($"{Name} Search | Language '{request.Language}' not supported, skip search."); + return Array.Empty(); + } + + string queryText; + if (config.EnableUseMetadata) + { + if (request.ContentType == VideoContentType.Episode) + { + queryText = $"{request.SeriesName} S{request.ParentIndexNumber:D2}E{request.IndexNumber:D2}"; + } + else if (request.ContentType == VideoContentType.Movie) + { + queryText = request.Name; + } + else + { + queryText = !string.IsNullOrEmpty(request.MediaPath) + ? Path.GetFileNameWithoutExtension(request.MediaPath) + : string.Empty; + } + } + else + { + queryText = !string.IsNullOrEmpty(request.MediaPath) + ? Path.GetFileNameWithoutExtension(request.MediaPath) + : string.Empty; + } + + if (string.IsNullOrWhiteSpace(queryText)) + { + _logger.Warn($"{Name} Search | Query text is empty, skip search."); + return Array.Empty(); + } + + _logger.Info($"{Name} Search | Target -> '{queryText}' | Language -> '{language}'"); + + var searchUrl = $"{ApiBaseUrl}/sub/search?token={Uri.EscapeDataString(config.AssrtApiToken)}&q={Uri.EscapeDataString(queryText)}&is_file=1&cnt=15"; + var maskedSearchUrl = $"{ApiBaseUrl}/sub/search?token={MaskToken(config.AssrtApiToken)}&q={Uri.EscapeDataString(queryText)}&is_file=1&cnt=15"; + _logger.Info($"{Name} Search | Request Url -> {maskedSearchUrl}"); + + var options = new HttpRequestOptions + { + Url = searchUrl, + UserAgent = Name, + TimeoutMs = 20000, + AcceptHeader = "application/json" + }; + + var response = await _httpClient.GetResponse(options); + if (response.StatusCode != HttpStatusCode.OK) + { + _logger.Error($"{Name} Search | API responded with status: {response.StatusCode}"); + return Array.Empty(); + } + + string jsonString; + using (var reader = new StreamReader(response.Content, Encoding.UTF8)) + { + jsonString = await reader.ReadToEndAsync(); + } + + _logger.Info($"{Name} Search | Response Status -> {response.StatusCode} | ResponseBody -> {jsonString}"); + + var searchResponse = _jsonSerializer.DeserializeFromString(jsonString); + + if (searchResponse == null || searchResponse.status != 0 || searchResponse.sub?.subs == null || !searchResponse.sub.subs.Any()) + { + _logger.Warn($"{Name} Search | No subtitles found or API error. Request -> Target: '{queryText}', Language: '{language}', Url: '{maskedSearchUrl}' | Response -> Status: {searchResponse?.status}, Content: '{jsonString}'"); + return Array.Empty(); + } + + var remoteSubtitles = new List(); + + foreach (var item in searchResponse.sub.subs) + { + if (item.id == null) continue; + + var subtitleId = item.id.ToString(); + var format = ExtractFormat(item.subtype) ?? ExtractFormat(item.name) ?? SRT; + + var langDesc = item.lang?.desc ?? "中文"; + + var downloadInfo = new AssrtDownloadInfo + { + Id = subtitleId, + Format = format, + Language = request.Language + }; + + var encodedId = Base64Encode(_jsonSerializer.SerializeToString(downloadInfo)); + + remoteSubtitles.Add(new RemoteSubtitleInfo + { + Id = encodedId, + Name = $"[MEIAMSUB] {item.name} | {langDesc} | 伪射手", + Author = "Assrt", + ProviderName = Name, + Format = format, + Comment = string.IsNullOrEmpty(item.videoname) ? $"格式: {format}" : $"适配视频: {item.videoname} (格式: {format})", + IsHashMatch = false + }); + } + + _logger.Info($"{Name} Search | Summary -> Found {remoteSubtitles.Count} subtitles."); + return remoteSubtitles; + } + catch (Exception ex) + { + _logger.Error($"{Name} Search | Exception occurred: [{ex.GetType().Name}] {ex.Message}"); + } + + return Array.Empty(); + } + + public async Task GetSubtitles(string id, CancellationToken cancellationToken) + { + _logger.Info($"{Name} GetSubtitles | Requesting ID: {id}"); + AssrtDownloadInfo downloadInfo = null; + + try + { + var decodedString = Base64Decode(id); + downloadInfo = _jsonSerializer.DeserializeFromString(decodedString); + if (downloadInfo == null || string.IsNullOrEmpty(downloadInfo.Id)) + { + return new SubtitleResponse { Language = "chi", Format = SRT, Stream = new MemoryStream() }; + } + + var config = Plugin.Instance?.Options; + if (config == null || string.IsNullOrWhiteSpace(config.AssrtApiToken)) + { + _logger.Error($"{Name} GetSubtitles | AssrtApiToken is not configured."); + return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() }; + } + + // 1. 请求详情接口获取下载地址 + var detailUrl = $"{ApiBaseUrl}/sub/detail?token={Uri.EscapeDataString(config.AssrtApiToken)}&id={Uri.EscapeDataString(downloadInfo.Id)}"; + + var detailOptions = new HttpRequestOptions + { + Url = detailUrl, + UserAgent = Name, + TimeoutMs = 20000, + AcceptHeader = "application/json" + }; + + var detailResponseMsg = await _httpClient.GetResponse(detailOptions); + if (detailResponseMsg.StatusCode != HttpStatusCode.OK) + { + _logger.Error($"{Name} GetSubtitles | Failed to fetch details. Status: {detailResponseMsg.StatusCode}"); + return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() }; + } + + string detailJson; + using (var reader = new StreamReader(detailResponseMsg.Content, Encoding.UTF8)) + { + detailJson = await reader.ReadToEndAsync(); + } + + var detailResponse = _jsonSerializer.DeserializeFromString(detailJson); + + var targetSub = detailResponse?.sub?.subs?.FirstOrDefault(); + if (targetSub == null || string.IsNullOrEmpty(targetSub.url)) + { + _logger.Error($"{Name} GetSubtitles | Subtitle download URL not found in API response."); + return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() }; + } + + _logger.Info($"{Name} GetSubtitles | Downloading subtitle from: {targetSub.url}"); + + // 2. 下载字幕流 (手动接管重定向以确保每次重定向请求均带有正确的防盗链 Headers) + var handler = new HttpClientHandler { AllowAutoRedirect = false }; + using var cleanHttpClient = new HttpClient(handler); + + var currentUrl = targetSub.url; + HttpResponseMessage downloadResponse = null; + int redirectCount = 0; + + while (redirectCount < 5) + { + var requestMsg = new HttpRequestMessage(HttpMethod.Get, currentUrl); + requestMsg.Version = HttpVersion.Version11; + requestMsg.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + requestMsg.Headers.Add("Referer", "https://assrt.net/"); + + downloadResponse = await cleanHttpClient.SendAsync(requestMsg, cancellationToken); + + if (downloadResponse.StatusCode == HttpStatusCode.Redirect || + downloadResponse.StatusCode == HttpStatusCode.MovedPermanently || + downloadResponse.StatusCode == HttpStatusCode.Found || + downloadResponse.StatusCode == HttpStatusCode.SeeOther || + (int)downloadResponse.StatusCode == 307 || + (int)downloadResponse.StatusCode == 308) + { + var location = downloadResponse.Headers.Location; + if (location != null) + { + if (!location.IsAbsoluteUri) + { + location = new Uri(new Uri(currentUrl), location); + } + currentUrl = location.AbsoluteUri; + redirectCount++; + _logger.Info($"{Name} GetSubtitles | Redirecting to -> {currentUrl}"); + downloadResponse.Dispose(); + continue; + } + } + break; + } + + if (downloadResponse == null || !downloadResponse.IsSuccessStatusCode) + { + _logger.Error($"{Name} GetSubtitles | Failed to download subtitle file. Status: {downloadResponse?.StatusCode}"); + return new SubtitleResponse { Language = downloadInfo.Language, Format = downloadInfo.Format, Stream = new MemoryStream() }; + } + + var responseStream = await downloadResponse.Content.ReadAsStreamAsync(); + + // 3. 防御性处理:检查是否为 ZIP 压缩包并提取 + var memoryStream = new MemoryStream(); + await responseStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + bool isZip = false; + if (memoryStream.Length > 4) + { + 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..."); + try + { + using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true)) + { + var entry = archive.Entries.FirstOrDefault(e => + e.FullName.EndsWith(".srt", StringComparison.OrdinalIgnoreCase) || + e.FullName.EndsWith(".ass", StringComparison.OrdinalIgnoreCase) || + e.FullName.EndsWith(".ssa", StringComparison.OrdinalIgnoreCase)); + + if (entry != null) + { + var extractedStream = new MemoryStream(); + using (var entryStream = entry.Open()) + { + await entryStream.CopyToAsync(extractedStream); + } + extractedStream.Position = 0; + var extFormat = ExtractFormat(entry.FullName) ?? downloadInfo.Format; + + _logger.Info($"{Name} GetSubtitles | Extracted file: '{entry.FullName}' with format: {extFormat}"); + + return new SubtitleResponse + { + Language = downloadInfo.Language, + IsForced = false, + Format = extFormat, + Stream = extractedStream + }; + } + else + { + _logger.Warn($"{Name} GetSubtitles | No srt/ass/ssa file found inside the ZIP package."); + } + } + } + catch (Exception ex) + { + _logger.Error($"{Name} GetSubtitles | Failed to extract ZIP archive: {ex.Message}"); + } + } + + // 4. 不是 ZIP 或者解压失败时,默认返回原流 + memoryStream.Position = 0; + return new SubtitleResponse + { + Language = downloadInfo.Language, + IsForced = false, + Format = downloadInfo.Format, + Stream = memoryStream + }; + } + catch (Exception ex) + { + _logger.Error($"{Name} GetSubtitles | Exception: {ex.Message}"); + } + + return new SubtitleResponse { Language = downloadInfo?.Language ?? "chi", Format = downloadInfo?.Format ?? SRT, Stream = new MemoryStream() }; + } + + #region 辅助工具函数 + + public static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return Encoding.UTF8.GetString(base64EncodedBytes); + } + + protected string ExtractFormat(string text) + { + if (string.IsNullOrEmpty(text)) + { + return null; + } + + text = text.ToLower(); + if (text.Contains(ASS)) return ASS; + if (text.Contains(SSA)) return SSA; + if (text.Contains(SRT)) return SRT; + + return null; + } + + private static string NormalizeLanguage(string language) + { + if (string.IsNullOrEmpty(language)) return language; + + if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh", StringComparison.OrdinalIgnoreCase) || + language.Equals("zho", StringComparison.OrdinalIgnoreCase) || + language.Equals("chi", StringComparison.OrdinalIgnoreCase)) + { + return "chi"; + } + if (language.Equals("en", StringComparison.OrdinalIgnoreCase) || + language.Equals("eng", StringComparison.OrdinalIgnoreCase)) + { + return "eng"; + } + return language; + } + + private static string MaskToken(string token) + { + if (string.IsNullOrEmpty(token)) return string.Empty; + if (token.Length <= 8) return "***"; + return token.Substring(0, 4) + "***" + token.Substring(token.Length - 4); + } + + #endregion + } +} diff --git a/Emby.MeiamSub.Assrt/Emby.MeiamSub.Assrt.csproj b/Emby.MeiamSub.Assrt/Emby.MeiamSub.Assrt.csproj new file mode 100644 index 0000000..8ed02b2 --- /dev/null +++ b/Emby.MeiamSub.Assrt/Emby.MeiamSub.Assrt.csproj @@ -0,0 +1,34 @@ + + + + 1.0.14.0 + 1.0.14.0 + 1.0.14.0 + + + + netstandard2.1 + + + + ..\Release + + + + + + + + + + + + + + + + + + + + diff --git a/Emby.MeiamSub.Assrt/Model/AssrtModels.cs b/Emby.MeiamSub.Assrt/Model/AssrtModels.cs new file mode 100644 index 0000000..6a0cc5d --- /dev/null +++ b/Emby.MeiamSub.Assrt/Model/AssrtModels.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +namespace Emby.MeiamSub.Assrt.Model +{ + public class AssrtSearchResponse + { + public int status { get; set; } + public AssrtSearchSubInfo sub { get; set; } + } + + public class AssrtSearchSubInfo + { + public string keyword { get; set; } + public string result { get; set; } + public List subs { get; set; } + } + + public class AssrtSearchItem + { + public object id { get; set; } + public string name { get; set; } + public string videoname { get; set; } + public string subtype { get; set; } + public AssrtLanguageInfo lang { get; set; } + } + + public class AssrtLanguageInfo + { + public string desc { get; set; } + public int lang { get; set; } + } + + public class AssrtDetailResponse + { + public int status { get; set; } + public AssrtDetailSubInfo sub { get; set; } + } + + public class AssrtDetailSubInfo + { + public List subs { get; set; } + } + + public class AssrtDetailItem + { + public object id { get; set; } + public string filename { get; set; } + public string url { get; set; } + } + + public class AssrtDownloadInfo + { + public string Id { get; set; } + public string Format { get; set; } + public string Language { get; set; } + } +} diff --git a/Emby.MeiamSub.Assrt/Plugin.cs b/Emby.MeiamSub.Assrt/Plugin.cs new file mode 100644 index 0000000..a1725de --- /dev/null +++ b/Emby.MeiamSub.Assrt/Plugin.cs @@ -0,0 +1,92 @@ +using Emby.Web.GenericEdit; +using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; +using System; +using System.ComponentModel; +using System.IO; + +namespace Emby.MeiamSub.Assrt +{ + /// + /// Assrt 插件入口 + /// + public class Plugin : BasePluginSimpleUI, IHasThumbImage + { + public Plugin(IApplicationPaths applicationPaths, IApplicationHost applicationHost) : base(applicationHost) + { + Instance = this; + } + + /// + /// 插件ID + /// + public override Guid Id => new Guid("D276A8D0-4B17-4EEF-94B6-61D646DE5A22"); + + /// + /// 插件名称 + /// + public override string Name => "MeiamSub.Assrt"; + + /// + /// 插件描述 + /// + public override string Description => "Download subtitles from Assrt (伪射手网)"; + + /// + /// 缩略图格式化类型 + /// + public ImageFormat ThumbImageFormat => ImageFormat.Gif; + + /// + /// 获取插件选项 + /// + public PluginConfiguration Options => this.GetOptions(); + + public static Plugin Instance { get; private set; } + + /// + /// 获取插件缩略图资源流 + /// + /// 图片资源流,若不存在则返回 null + public Stream GetThumbImage() + { + var type = GetType(); + var resourceName = $"{type.Namespace}.Thumb.png"; + var stream = type.Assembly.GetManifestResourceStream(resourceName); + + if (stream == null) + { + return null; + } + + return stream; + } + } + + /// + /// 插件配置类 + /// + public class PluginConfiguration : EditableOptionsBase + { + public override string EditorTitle => "MeiamSub Assrt Options"; + + [DisplayName("Assrt API Token")] + [Description("请输入您在 assrt.net 注册获取的 32 位 API Token")] + public string AssrtApiToken { get; set; } + + [DisplayName("使用元数据搜索字幕")] + [Description("勾选此项后,使用元数据中的剧集名称和季集编号搜索字幕,而非文件名。建议开启。")] + public bool EnableUseMetadata { get; set; } + + public PluginConfiguration() + { + AssrtApiToken = string.Empty; + EnableUseMetadata = true; + } + } +} diff --git a/Emby.MeiamSub.Assrt/Thumb.png b/Emby.MeiamSub.Assrt/Thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..34cbf7bcf9eeb37fd5d198458f6fed9acd187297 GIT binary patch literal 17825 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%0AJWU2CzJVsZ2`WwzLvwp5vZFQw_j?(gPp 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 literal 0 HcmV?d00001 diff --git a/Emby.MeiamSub.Shooter/Emby.MeiamSub.Shooter.csproj b/Emby.MeiamSub.Shooter/Emby.MeiamSub.Shooter.csproj index 2a577c7..8ed02b2 100644 --- a/Emby.MeiamSub.Shooter/Emby.MeiamSub.Shooter.csproj +++ b/Emby.MeiamSub.Shooter/Emby.MeiamSub.Shooter.csproj @@ -28,7 +28,7 @@ - + diff --git a/Emby.MeiamSub.Shooter/Plugin.cs b/Emby.MeiamSub.Shooter/Plugin.cs index 3e9904f..f05ac00 100644 --- a/Emby.MeiamSub.Shooter/Plugin.cs +++ b/Emby.MeiamSub.Shooter/Plugin.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Drawing; using System; diff --git a/Emby.MeiamSub.Thunder/Emby.MeiamSub.Thunder.csproj b/Emby.MeiamSub.Thunder/Emby.MeiamSub.Thunder.csproj index 984de0f..2812b0b 100644 --- a/Emby.MeiamSub.Thunder/Emby.MeiamSub.Thunder.csproj +++ b/Emby.MeiamSub.Thunder/Emby.MeiamSub.Thunder.csproj @@ -34,7 +34,7 @@ - + diff --git a/Jellyfin.MeiamSub.Assrt/AssrtProvider.cs b/Jellyfin.MeiamSub.Assrt/AssrtProvider.cs new file mode 100644 index 0000000..7f0c4ae --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/AssrtProvider.cs @@ -0,0 +1,409 @@ +using Jellyfin.MeiamSub.Assrt.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.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Jellyfin.MeiamSub.Assrt +{ + /// + /// Assrt (伪射手) 字幕提供程序 + /// + public class AssrtProvider : ISubtitleProvider, IHasOrder + { + private const string ApiBaseUrl = "https://api.assrt.net/v1"; + + public const string ASS = "ass"; + public const string SSA = "ssa"; + public const string SRT = "srt"; + + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public int Order => 90; // 优先级高于旧版 + + public string Name => "MeiamSub.Assrt"; + + public IEnumerable SupportedMediaTypes => new[] { VideoContentType.Movie, VideoContentType.Episode }; + + public AssrtProvider(ILogger logger, IHttpClientFactory httpClientFactory) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _logger.LogInformation($"{Name} Init"); + } + + public async Task> Search(SubtitleSearchRequest request, CancellationToken cancellationToken) + { + _logger.LogInformation($"{Name} Search | Received request for: {(request?.MediaPath ?? "NULL")}"); + + try + { + if (request == null) + { + return Array.Empty(); + } + + var config = Plugin.Instance?.Configuration; + if (config == null || string.IsNullOrWhiteSpace(config.AssrtApiToken)) + { + _logger.LogWarning($"{Name} Search | AssrtApiToken is empty, skip search. Please configure the token in plugin settings."); + return Array.Empty(); + } + + var language = NormalizeLanguage(request.Language); + if (language != "chi") + { + _logger.LogInformation($"{Name} Search | Language '{request.Language}' not supported, skip search."); + return Array.Empty(); + } + + string queryText; + if (config.EnableUseMetadata) + { + if (request.ContentType == VideoContentType.Episode) + { + queryText = $"{request.SeriesName} S{request.ParentIndexNumber:D2}E{request.IndexNumber:D2}"; + } + else if (request.ContentType == VideoContentType.Movie) + { + queryText = request.Name; + } + else + { + queryText = !string.IsNullOrEmpty(request.MediaPath) + ? Path.GetFileNameWithoutExtension(request.MediaPath) + : string.Empty; + } + } + else + { + queryText = !string.IsNullOrEmpty(request.MediaPath) + ? Path.GetFileNameWithoutExtension(request.MediaPath) + : string.Empty; + } + + if (string.IsNullOrWhiteSpace(queryText)) + { + _logger.LogWarning($"{Name} Search | Query text is empty, skip search."); + return Array.Empty(); + } + + _logger.LogInformation($"{Name} Search | Target -> '{queryText}' | Language -> '{language}'"); + + using var httpClient = _httpClientFactory.CreateClient(Name); + + // 拼接搜索请求,is_file=1 用于对文件名检索进行关键词降噪 + var searchUrl = $"{ApiBaseUrl}/sub/search?token={Uri.EscapeDataString(config.AssrtApiToken)}&q={Uri.EscapeDataString(queryText)}&is_file=1&cnt=15"; + + // 隐藏敏感 Token 信息以便记录日志 + var maskedSearchUrl = $"{ApiBaseUrl}/sub/search?token={MaskToken(config.AssrtApiToken)}&q={Uri.EscapeDataString(queryText)}&is_file=1&cnt=15"; + _logger.LogInformation($"{Name} Search | Request Url -> {maskedSearchUrl}"); + + var response = await httpClient.GetAsync(searchUrl, cancellationToken); + var jsonString = await response.Content.ReadAsStringAsync(cancellationToken); + + _logger.LogInformation($"{Name} Search | Response Status -> {response.StatusCode} | ResponseBody -> {jsonString}"); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError($"{Name} Search | API responded with status: {response.StatusCode}"); + return Array.Empty(); + } + + var searchResponse = JsonSerializer.Deserialize(jsonString); + + if (searchResponse == null || searchResponse.Status != 0 || searchResponse.Sub?.List == null || !searchResponse.Sub.List.Any()) + { + _logger.LogWarning($"{Name} Search | No subtitles found or API error. Request -> Target: '{queryText}', Language: '{language}', Url: '{maskedSearchUrl}' | Response -> Status: {searchResponse?.Status}, Content: '{jsonString}'"); + return Array.Empty(); + } + + var remoteSubtitles = new List(); + + foreach (var item in searchResponse.Sub.List) + { + if (item.Id == null) continue; + + var subtitleId = item.Id.ToString(); + var format = ExtractFormat(item.Subtype) ?? ExtractFormat(item.Name) ?? SRT; + + var langDesc = item.Lang?.Desc ?? "中文"; + + var downloadInfo = new AssrtDownloadInfo + { + Id = subtitleId, + Format = format, + Language = request.Language, + TwoLetterISOLanguageName = request.TwoLetterISOLanguageName + }; + + var encodedId = Base64Encode(JsonSerializer.Serialize(downloadInfo)); + + remoteSubtitles.Add(new RemoteSubtitleInfo + { + Id = encodedId, + Name = $"[MEIAMSUB] {item.Name} | {langDesc} | 伪射手", + Author = "Assrt", + ProviderName = Name, + Format = format, + Comment = string.IsNullOrEmpty(item.VideoName) ? $"格式: {format}" : $"适配视频: {item.VideoName} (格式: {format})", + IsHashMatch = false // Assrt.net 接口主要是文本搜索,返回 false 以提示用户是名字检索匹配 + }); + } + + _logger.LogInformation($"{Name} Search | Summary -> Found {remoteSubtitles.Count} subtitles."); + return remoteSubtitles; + } + catch (Exception ex) + { + _logger.LogError(ex, $"{Name} Search | Exception occurred: {ex.Message}"); + } + + return Array.Empty(); + } + + public async Task GetSubtitles(string id, CancellationToken cancellationToken) + { + _logger.LogInformation($"{Name} GetSubtitles | Requesting ID: {id}"); + AssrtDownloadInfo downloadInfo = null; + + try + { + var decodedString = Base64Decode(id); + downloadInfo = JsonSerializer.Deserialize(decodedString); + if (downloadInfo == null || string.IsNullOrEmpty(downloadInfo.Id)) + { + throw new ArgumentException($"{Name} GetSubtitles | 无法解析字幕下载信息,ID 为空。"); + } + + var config = Plugin.Instance?.Configuration; + if (config == null || string.IsNullOrWhiteSpace(config.AssrtApiToken)) + { + _logger.LogError($"{Name} GetSubtitles | AssrtApiToken is not configured."); + throw new InvalidOperationException($"{Name} GetSubtitles | AssrtApiToken 未配置,请在插件设置中填写 Token。"); + } + + using var httpClient = _httpClientFactory.CreateClient(Name); + + // 1. 请求详情接口获取真实的下载地址 + var detailUrl = $"{ApiBaseUrl}/sub/detail?token={Uri.EscapeDataString(config.AssrtApiToken)}&id={Uri.EscapeDataString(downloadInfo.Id)}"; + var detailResponseMsg = await httpClient.GetAsync(detailUrl, cancellationToken); + if (!detailResponseMsg.IsSuccessStatusCode) + { + _logger.LogError($"{Name} GetSubtitles | Failed to fetch details. Status: {detailResponseMsg.StatusCode}"); + throw new HttpRequestException($"{Name} GetSubtitles | 获取字幕详情失败,HTTP 状态码: {detailResponseMsg.StatusCode}"); + } + + var detailJson = await detailResponseMsg.Content.ReadAsStringAsync(cancellationToken); + var detailResponse = JsonSerializer.Deserialize(detailJson); + + var targetSub = detailResponse?.Sub?.Subs?.FirstOrDefault(); + if (targetSub == null || string.IsNullOrEmpty(targetSub.Url)) + { + _logger.LogError($"{Name} GetSubtitles | Subtitle download URL not found in API response."); + throw new InvalidOperationException($"{Name} GetSubtitles | API 响应中未找到字幕下载地址。"); + } + + _logger.LogInformation($"{Name} GetSubtitles | Downloading subtitle from: {targetSub.Url}"); + + // 2. 下载字幕流 (手动接管重定向以确保每次重定向请求均带有正确的防盗链 Headers) + var handler = new HttpClientHandler { AllowAutoRedirect = false }; + using var cleanHttpClient = new HttpClient(handler); + + var currentUrl = targetSub.Url; + + HttpResponseMessage downloadResponse = null; + int redirectCount = 0; + + while (redirectCount < 5) + { + var requestMsg = new HttpRequestMessage(HttpMethod.Get, currentUrl); + requestMsg.Version = HttpVersion.Version11; + requestMsg.Headers.ConnectionClose = true; // 强制关闭长连接,防范服务器 Keep-Alive 超时关闭 + requestMsg.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + requestMsg.Headers.Add("Referer", "https://assrt.net/"); + + downloadResponse = await cleanHttpClient.SendAsync(requestMsg, cancellationToken); + + if (downloadResponse.StatusCode == HttpStatusCode.Redirect || + downloadResponse.StatusCode == HttpStatusCode.MovedPermanently || + downloadResponse.StatusCode == HttpStatusCode.Found || + downloadResponse.StatusCode == HttpStatusCode.SeeOther || + (int)downloadResponse.StatusCode == 307 || + (int)downloadResponse.StatusCode == 308) + { + var location = downloadResponse.Headers.Location; + if (location != null) + { + if (!location.IsAbsoluteUri) + { + location = new Uri(new Uri(currentUrl), location); + } + currentUrl = location.AbsoluteUri; + redirectCount++; + _logger.LogInformation($"{Name} GetSubtitles | Redirecting to -> {currentUrl}"); + downloadResponse.Dispose(); + continue; + } + } + break; + } + + if (downloadResponse == null || !downloadResponse.IsSuccessStatusCode) + { + _logger.LogError($"{Name} GetSubtitles | Failed to download subtitle file. Status: {downloadResponse?.StatusCode}"); + throw new HttpRequestException($"{Name} GetSubtitles | 字幕文件下载失败,HTTP 状态码: {downloadResponse?.StatusCode}"); + } + + var responseStream = await downloadResponse.Content.ReadAsStreamAsync(cancellationToken); + + // 3. 防御性处理:检查是否为 ZIP 压缩包 + // 如果是 ZIP,解压出里面第一个符合格式要求的字幕流 + var memoryStream = new MemoryStream(); + await responseStream.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + // 判断 ZIP 标志 (PK.. / 0x50 0x4B) + bool isZip = false; + if (memoryStream.Length > 4) + { + 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..."); + try + { + using var archive = new ZipArchive(memoryStream, ZipArchiveMode.Read, true); + var entry = archive.Entries.FirstOrDefault(e => + e.FullName.EndsWith(".srt", StringComparison.OrdinalIgnoreCase) || + e.FullName.EndsWith(".ass", StringComparison.OrdinalIgnoreCase) || + e.FullName.EndsWith(".ssa", StringComparison.OrdinalIgnoreCase)); + + if (entry != null) + { + var extractedStream = new MemoryStream(); + using (var entryStream = entry.Open()) + { + await entryStream.CopyToAsync(extractedStream, cancellationToken); + } + extractedStream.Position = 0; + var extFormat = ExtractFormat(entry.FullName) ?? downloadInfo.Format; + + _logger.LogInformation($"{Name} GetSubtitles | Extracted file: '{entry.FullName}' with format: {extFormat}"); + + return new SubtitleResponse + { + Language = downloadInfo.Language, + IsForced = false, + Format = extFormat, + Stream = extractedStream + }; + } + else + { + _logger.LogWarning($"{Name} GetSubtitles | No srt/ass/ssa file found inside the ZIP package."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"{Name} GetSubtitles | Failed to extract ZIP archive: {ex.Message}"); + } + } + + // 4. 不是 ZIP 或者解压失败时,默认返回原流 + memoryStream.Position = 0; + return new SubtitleResponse + { + Language = downloadInfo.Language, + IsForced = false, + Format = downloadInfo.Format, + Stream = memoryStream + }; + } + catch (Exception ex) + { + _logger.LogError(ex, $"{Name} GetSubtitles | Exception: {ex.Message}"); + throw; + } + } + + #region 辅助工具函数 + + public static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string Base64Decode(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return Encoding.UTF8.GetString(base64EncodedBytes); + } + + protected string ExtractFormat(string text) + { + if (string.IsNullOrEmpty(text)) + { + return null; + } + + text = text.ToLower(); + if (text.Contains(ASS)) return ASS; + if (text.Contains(SSA)) return SSA; + if (text.Contains(SRT)) return SRT; + + return null; + } + + private static string NormalizeLanguage(string language) + { + if (string.IsNullOrEmpty(language)) return language; + + if (language.Equals("zh-CN", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-TW", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh-HK", StringComparison.OrdinalIgnoreCase) || + language.Equals("zh", StringComparison.OrdinalIgnoreCase) || + language.Equals("zho", StringComparison.OrdinalIgnoreCase) || + language.Equals("chi", StringComparison.OrdinalIgnoreCase)) + { + return "chi"; + } + if (language.Equals("en", StringComparison.OrdinalIgnoreCase) || + language.Equals("eng", StringComparison.OrdinalIgnoreCase)) + { + return "eng"; + } + return language; + } + + private static string MaskToken(string token) + { + if (string.IsNullOrEmpty(token)) return string.Empty; + if (token.Length <= 8) return "***"; + return token.Substring(0, 4) + "***" + token.Substring(token.Length - 4); + } + + #endregion + } +} diff --git a/Jellyfin.MeiamSub.Assrt/Configuration/PluginConfiguration.cs b/Jellyfin.MeiamSub.Assrt/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000..65e8e0c --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/Configuration/PluginConfiguration.cs @@ -0,0 +1,26 @@ +using MediaBrowser.Model.Plugins; + +namespace Jellyfin.MeiamSub.Assrt.Configuration +{ + /// + /// 插件配置类 + /// + public class PluginConfiguration : BasePluginConfiguration + { + /// + /// Assrt API 鉴权 Token + /// + public string AssrtApiToken { get; set; } + + /// + /// 是否使用元数据中的剧集名称和季集编号搜索字幕 + /// + public bool EnableUseMetadata { get; set; } + + public PluginConfiguration() + { + AssrtApiToken = string.Empty; + EnableUseMetadata = true; // 默认为 true,因为 Assrt 是基于文本搜索,元数据匹配非常合适 + } + } +} diff --git a/Jellyfin.MeiamSub.Assrt/Configuration/configPage.html b/Jellyfin.MeiamSub.Assrt/Configuration/configPage.html new file mode 100644 index 0000000..aae5215 --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/Configuration/configPage.html @@ -0,0 +1,76 @@ + + + + MeiamSub.Assrt + + +
+
+
+
+
+

伪射手字幕搜索设置 (Assrt)

+ +
+ + +
+ 请在 assrt.net 注册并获取你的 32 位 API Token。 +
+
+ +
+ +
+ 勾选此项后,使用元数据中的剧集名称和季集编号搜索字幕,而非文件名。 +
+ 剧集将使用 "剧集名 S01E01" 格式;电影将使用影片名称。建议开启以提升模糊检索成功率。 +
+
+
+
+ +
+
+
+
+ + +
+ + diff --git a/Jellyfin.MeiamSub.Assrt/Jellyfin.MeiamSub.Assrt.csproj b/Jellyfin.MeiamSub.Assrt/Jellyfin.MeiamSub.Assrt.csproj new file mode 100644 index 0000000..7bc7797 --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/Jellyfin.MeiamSub.Assrt.csproj @@ -0,0 +1,33 @@ + + + + Library + net9.0 + 1.0.14.0 + 1.0.14.0 + 1.0.14.0 + + + + ..\Release + + + + + + + + + PreserveNewest + + + + + + + + + + + + diff --git a/Jellyfin.MeiamSub.Assrt/Model/AssrtModels.cs b/Jellyfin.MeiamSub.Assrt/Model/AssrtModels.cs new file mode 100644 index 0000000..af6cded --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/Model/AssrtModels.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Jellyfin.MeiamSub.Assrt.Model +{ + /// + /// 字幕搜索响应根结构 + /// + public class AssrtSearchResponse + { + [JsonPropertyName("status")] + public int Status { get; set; } + + [JsonPropertyName("sub")] + public AssrtSearchSubInfo Sub { get; set; } + } + + public class AssrtSearchSubInfo + { + [JsonPropertyName("keyword")] + public string Keyword { get; set; } + + [JsonPropertyName("result")] + public string Result { get; set; } + + [JsonPropertyName("subs")] + public List List { get; set; } + } + + public class AssrtSearchItem + { + [JsonPropertyName("id")] + public object Id { get; set; } // 可以是数字也可以是字符串,用 object 接收再 ToString() + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("videoname")] + public string VideoName { get; set; } + + [JsonPropertyName("subtype")] + public string Subtype { get; set; } + + [JsonPropertyName("lang")] + public AssrtLanguageInfo Lang { get; set; } + } + + public class AssrtLanguageInfo + { + [JsonPropertyName("desc")] + public string Desc { get; set; } + + [JsonPropertyName("lang")] + public int Lang { get; set; } + } + + /// + /// 字幕详情响应根结构 + /// + public class AssrtDetailResponse + { + [JsonPropertyName("status")] + public int Status { get; set; } + + [JsonPropertyName("sub")] + public AssrtDetailSubInfo Sub { get; set; } + } + + public class AssrtDetailSubInfo + { + [JsonPropertyName("subs")] + public List Subs { get; set; } + } + + public class AssrtDetailItem + { + [JsonPropertyName("id")] + public object Id { get; set; } + + [JsonPropertyName("filename")] + public string Filename { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + } + + /// + /// 插件内部传递的下载参数 + /// + public class AssrtDownloadInfo + { + public string Id { get; set; } + public string Format { get; set; } + public string Language { get; set; } + public string TwoLetterISOLanguageName { get; set; } + } +} diff --git a/Jellyfin.MeiamSub.Assrt/Plugin.cs b/Jellyfin.MeiamSub.Assrt/Plugin.cs new file mode 100644 index 0000000..777606d --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/Plugin.cs @@ -0,0 +1,55 @@ +using Jellyfin.MeiamSub.Assrt.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Plugins; +using MediaBrowser.Model.Serialization; +using System; +using System.Collections.Generic; + +namespace Jellyfin.MeiamSub.Assrt +{ + /// + /// Assrt 插件入口 + /// + public class Plugin : BasePlugin, IHasWebPages + { + /// + /// 插件ID + /// + public override Guid Id => new Guid("E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0"); + + /// + /// 插件名称 + /// + public override string Name => "MeiamSub.Assrt"; + + /// + /// 插件描述 + /// + public override string Description => "Download subtitles from Assrt (伪射手网)"; + + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + public static Plugin Instance { get; private set; } + + /// + /// 获取插件配置页面列表 + /// + /// 配置页面信息 + public IEnumerable GetPages() + { + return new[] + { + new PluginPageInfo + { + Name = Name, + EmbeddedResourcePath = GetType().Namespace + ".Configuration.configPage.html" + } + }; + } + } +} diff --git a/Jellyfin.MeiamSub.Assrt/PluginServiceRegistrator.cs b/Jellyfin.MeiamSub.Assrt/PluginServiceRegistrator.cs new file mode 100644 index 0000000..b1054c7 --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/PluginServiceRegistrator.cs @@ -0,0 +1,26 @@ +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Controller; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Jellyfin.MeiamSub.Assrt +{ + /// + /// 插件服务注册器 + /// + public class PluginServiceRegistrator : IPluginServiceRegistrator + { + public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) + { + serviceCollection.AddHttpClient("MeiamSub.Assrt", client => + { + client.Timeout = TimeSpan.FromSeconds(20); // 适当的超时时间 + client.DefaultRequestHeaders.Add("User-Agent", "MeiamSub.Assrt/1.0.14.0 (Jellyfin)"); + client.DefaultRequestHeaders.Add("Accept", "*/*"); + }); + + serviceCollection.AddSingleton(); + } + } +} diff --git a/Jellyfin.MeiamSub.Assrt/meta.json b/Jellyfin.MeiamSub.Assrt/meta.json new file mode 100644 index 0000000..f11a4b7 --- /dev/null +++ b/Jellyfin.MeiamSub.Assrt/meta.json @@ -0,0 +1,12 @@ +{ + "category": "Subtitles", + "description": "Download subtitles from Assrt (伪射手网)", + "guid": "E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0", + "name": "MeiamSub.Assrt", + "owner": "Meiam", + "targetAbi": "10.11.0.0", + "timestamp": "2026-06-11T03:59:00.0000000Z", + "version": "1.0.14.0", + "status": "Active", + "imagePath": "thumb.png" +} diff --git a/Jellyfin.MeiamSub.Assrt/thumb.png b/Jellyfin.MeiamSub.Assrt/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..ec89d6f5b1bceb1f889f91e7ec3a94c0e59c1e79 GIT binary patch literal 18337 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 - + diff --git a/Jellyfin.MeiamSub.Shooter/Plugin.cs b/Jellyfin.MeiamSub.Shooter/Plugin.cs index 4da30a3..2767de2 100644 --- a/Jellyfin.MeiamSub.Shooter/Plugin.cs +++ b/Jellyfin.MeiamSub.Shooter/Plugin.cs @@ -1,4 +1,4 @@ -using Jellyfin.MeiamSub.Shooter.Configuration; +using Jellyfin.MeiamSub.Shooter.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Serialization; diff --git a/Jellyfin.MeiamSub.Thunder/Jellyfin.MeiamSub.Thunder.csproj b/Jellyfin.MeiamSub.Thunder/Jellyfin.MeiamSub.Thunder.csproj index 8698de6..28d8906 100644 --- a/Jellyfin.MeiamSub.Thunder/Jellyfin.MeiamSub.Thunder.csproj +++ b/Jellyfin.MeiamSub.Thunder/Jellyfin.MeiamSub.Thunder.csproj @@ -26,7 +26,7 @@ - + diff --git a/MeiamSubtitles.sln b/MeiamSubtitles.sln index 81561d8..1b643e1 100644 --- a/MeiamSubtitles.sln +++ b/MeiamSubtitles.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.34916.146 @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.MeiamSub.Shooter", "Em EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.MeiamSub.Thunder", "Emby.MeiamSub.Thunder\Emby.MeiamSub.Thunder.csproj", "{96F4C65C-11B1-46F4-B343-115168688C2D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.MeiamSub.Assrt", "Jellyfin.MeiamSub.Assrt\Jellyfin.MeiamSub.Assrt.csproj", "{E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.MeiamSub.Assrt", "Emby.MeiamSub.Assrt\Emby.MeiamSub.Assrt.csproj", "{D276A8D0-4B17-4EEF-94B6-61D646DE5A22}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +43,14 @@ Global {96F4C65C-11B1-46F4-B343-115168688C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {96F4C65C-11B1-46F4-B343-115168688C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {96F4C65C-11B1-46F4-B343-115168688C2D}.Release|Any CPU.Build.0 = Release|Any CPU + {E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E693CDAB-8E8C-4D12-BBDF-3FE564CD61F0}.Release|Any CPU.Build.0 = Release|Any CPU + {D276A8D0-4B17-4EEF-94B6-61D646DE5A22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/publish.ps1 b/publish.ps1 index 55f3a6c..c682a29 100644 --- a/publish.ps1 +++ b/publish.ps1 @@ -28,14 +28,18 @@ $ThunderCsproj = "Jellyfin.MeiamSub.Thunder\Jellyfin.MeiamSub.Thunder.csproj" $ThunderMeta = "Jellyfin.MeiamSub.Thunder\meta.json" $ShooterCsproj = "Jellyfin.MeiamSub.Shooter\Jellyfin.MeiamSub.Shooter.csproj" $ShooterMeta = "Jellyfin.MeiamSub.Shooter\meta.json" +$AssrtCsproj = "Jellyfin.MeiamSub.Assrt\Jellyfin.MeiamSub.Assrt.csproj" +$AssrtMeta = "Jellyfin.MeiamSub.Assrt\meta.json" $EmbyThunderCsproj = "Emby.MeiamSub.Thunder\Emby.MeiamSub.Thunder.csproj" $EmbyShooterCsproj = "Emby.MeiamSub.Shooter\Emby.MeiamSub.Shooter.csproj" +$EmbyAssrtCsproj = "Emby.MeiamSub.Assrt\Emby.MeiamSub.Assrt.csproj" # Update NuGet package version if specified if ($JellyfinVersion) { Write-Host "Upgrading Jellyfin.Controller NuGet dependency to $JellyfinVersion..." -ForegroundColor Yellow (Get-Content $ThunderCsproj) -replace '', "" | Set-Content $ThunderCsproj (Get-Content $ShooterCsproj) -replace '', "" | Set-Content $ShooterCsproj + (Get-Content $AssrtCsproj) -replace '', "" | Set-Content $AssrtCsproj } if ($EmbyVersion) { @@ -44,6 +48,8 @@ if ($EmbyVersion) { -replace '', "" | Set-Content $EmbyThunderCsproj (Get-Content $EmbyShooterCsproj) -replace '', "" ` -replace '', "" | Set-Content $EmbyShooterCsproj + (Get-Content $EmbyAssrtCsproj) -replace '', "" ` + -replace '', "" | Set-Content $EmbyAssrtCsproj } Write-Host "Updating version and timestamp in project files..." -ForegroundColor Yellow @@ -63,6 +69,14 @@ Write-Host "Updating version and timestamp in project files..." -ForegroundColor (Get-Content $ShooterMeta) -replace '"version":\s*"[\d\.]+"', "`"version`": `"$Version`"" ` -replace '"timestamp":\s*"[^"]+"', "`"timestamp`": `"$Timestamp`"" | Set-Content $ShooterMeta +# Update Jellyfin Assrt +(Get-Content $AssrtCsproj) -replace '[\d\.]+', "$Version" ` + -replace '[\d\.]+', "$Version" ` + -replace '[\d\.]+', "$Version" | Set-Content $AssrtCsproj + +(Get-Content $AssrtMeta) -replace '"version":\s*"[\d\.]+"', "`"version`": `"$Version`"" ` + -replace '"timestamp":\s*"[^"]+"', "`"timestamp`": `"$Timestamp`"" | Set-Content $AssrtMeta + # Update Emby Thunder (Get-Content $EmbyThunderCsproj) -replace '[\d\.]+', "$Version" ` -replace '[\d\.]+', "$Version" ` @@ -73,6 +87,11 @@ Write-Host "Updating version and timestamp in project files..." -ForegroundColor -replace '[\d\.]+', "$Version" ` -replace '[\d\.]+', "$Version" | Set-Content $EmbyShooterCsproj +# Update Emby Assrt +(Get-Content $EmbyAssrtCsproj) -replace '[\d\.]+', "$Version" ` + -replace '[\d\.]+', "$Version" ` + -replace '[\d\.]+', "$Version" | Set-Content $EmbyAssrtCsproj + Write-Host "Versions and timestamps updated successfully!" -ForegroundColor Green # 2. Compile solution @@ -85,16 +104,19 @@ $TempDir = "Release\temp_pack" $EmbyPackDir = "$TempDir\Emby" $JellyfinShooterPackDir = "$TempDir\Jellyfin\Jellyfin.MeiamSub.Shooter_$Version" $JellyfinThunderPackDir = "$TempDir\Jellyfin\Jellyfin.MeiamSub.Thunder_$Version" +$JellyfinAssrtPackDir = "$TempDir\Jellyfin\Jellyfin.MeiamSub.Assrt_$Version" # Clean temp directories if (Test-Path $TempDir) { Remove-Item -Recurse -Force $TempDir } New-Item -ItemType Directory -Force -Path $EmbyPackDir | Out-Null New-Item -ItemType Directory -Force -Path $JellyfinShooterPackDir | Out-Null New-Item -ItemType Directory -Force -Path $JellyfinThunderPackDir | Out-Null +New-Item -ItemType Directory -Force -Path $JellyfinAssrtPackDir | Out-Null # Copy Emby DLLs Copy-Item Release\Emby.MeiamSub.Shooter.dll $EmbyPackDir\ Copy-Item Release\Emby.MeiamSub.Thunder.dll $EmbyPackDir\ +Copy-Item Release\Emby.MeiamSub.Assrt.dll $EmbyPackDir\ # Copy Jellyfin Shooter artifacts Copy-Item Release\Jellyfin.MeiamSub.Shooter.dll $JellyfinShooterPackDir\ @@ -106,6 +128,11 @@ Copy-Item Release\Jellyfin.MeiamSub.Thunder.dll $JellyfinThunderPackDir\ Copy-Item Jellyfin.MeiamSub.Thunder\meta.json $JellyfinThunderPackDir\ Copy-Item Jellyfin.MeiamSub.Thunder\thumb.png $JellyfinThunderPackDir\ +# Copy Jellyfin Assrt artifacts +Copy-Item Release\Jellyfin.MeiamSub.Assrt.dll $JellyfinAssrtPackDir\ +Copy-Item Jellyfin.MeiamSub.Assrt\meta.json $JellyfinAssrtPackDir\ +Copy-Item Jellyfin.MeiamSub.Assrt\thumb.png $JellyfinAssrtPackDir\ + # Archive target paths $EmbyZip = "Release\Emby_v$Version.zip" $JellyfinZip = "Release\Jellyfin_v$Version.zip"