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 0000000..34cbf7b Binary files /dev/null and b/Emby.MeiamSub.Assrt/Thumb.png differ 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 API Token + + + 请在 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 0000000..ec89d6f Binary files /dev/null and b/Jellyfin.MeiamSub.Assrt/thumb.png differ diff --git a/Jellyfin.MeiamSub.Shooter/Jellyfin.MeiamSub.Shooter.csproj b/Jellyfin.MeiamSub.Shooter/Jellyfin.MeiamSub.Shooter.csproj index c2cbef9..8a0b0ba 100644 --- a/Jellyfin.MeiamSub.Shooter/Jellyfin.MeiamSub.Shooter.csproj +++ b/Jellyfin.MeiamSub.Shooter/Jellyfin.MeiamSub.Shooter.csproj @@ -22,7 +22,7 @@ - + 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"