From 3d47e802f1264891e77ef379879a70d8e0963452 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 15 May 2026 22:11:17 +0300 Subject: [PATCH 1/3] feat(streamdata): add new StreamData online provider module Introduce a full LME.StreamData module with initialization, manifest, and online registration to expose StreamData as a new content source. Implement TMDB-based movie, series, and episode retrieval flows with controller endpoints, API invoke client, proxy-aware requests, caching, and subtitle/stream mapping for playback templates. --- LME.StreamData/Controller.cs | 294 ++++++++++++++++++++++ LME.StreamData/ModInit.cs | 90 +++++++ LME.StreamData/Models/StreamDataModels.cs | 31 +++ LME.StreamData/OnlineApi.cs | 28 +++ LME.StreamData/StreamData.csproj | 15 ++ LME.StreamData/StreamDataInvoke.cs | 175 +++++++++++++ LME.StreamData/manifest.json | 12 + 7 files changed, 645 insertions(+) create mode 100644 LME.StreamData/Controller.cs create mode 100644 LME.StreamData/ModInit.cs create mode 100644 LME.StreamData/Models/StreamDataModels.cs create mode 100644 LME.StreamData/OnlineApi.cs create mode 100644 LME.StreamData/StreamData.csproj create mode 100644 LME.StreamData/StreamDataInvoke.cs create mode 100644 LME.StreamData/manifest.json diff --git a/LME.StreamData/Controller.cs b/LME.StreamData/Controller.cs new file mode 100644 index 0000000..80ffaad --- /dev/null +++ b/LME.StreamData/Controller.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using LME.StreamData.Models; +using Microsoft.AspNetCore.Mvc; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Online.Settings; +using Shared.Models.Templates; + +namespace LME.StreamData.Controllers +{ + public class Controller : BaseOnlineController + { + ProxyManager proxyManager; + + public Controller() : base(ModInit.Settings) + { + proxyManager = new ProxyManager(ModInit.StreamDataSettings); + } + + /// + /// Головний ендпоінт модуля StreamData + /// Працює виключно через TMDB ID (параметр id) + /// + [HttpGet] + [Route("lite/lme_streamdata")] + async public Task Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int e = -1, bool play = false, bool rjson = false, string href = null, bool checksearch = false) + { + await UpdateService.ConnectAsync(host); + + var init = loadKit(ModInit.StreamDataSettings); + if (!init.enable) + return Forbid(); + + var invoke = new StreamDataInvoke(init, hybridCache, OnLog, proxyManager, httpHydra); + + // checksearch — перевірка доступності + if (checksearch) + { + if (!IsCheckOnlineSearchEnabled()) + return OnError("lme_streamdata", refresh_proxy: true); + + if (id > 0) + return Content("data-json=", "text/plain; charset=utf-8"); + + return OnError("lme_streamdata", refresh_proxy: true); + } + + // play — повернути стрім для конкретного епізоду (call метод) + if (play) + { + return await HandlePlay(invoke, init, id, s, e, title, original_title); + } + + // Фільм + if (serial != 1) + { + return await HandleMovie(invoke, init, id, title, original_title, rjson); + } + + // Серіал + return await HandleSerial(invoke, init, id, title, original_title, s, e, t, rjson); + } + + /// + /// Обробка фільму: отримуємо всі stream_urls та показуємо їх + /// + private async Task HandleMovie(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, string title, string originalTitle, bool rjson) + { + var response = await invoke.GetMovie(tmdbId); + if (response?.data?.stream_urls == null || response.data.stream_urls.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + var streamUrls = response.data.stream_urls; + var subs = CollectSubtitles(response); + + var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); + var tpl = new MovieTpl(displayTitle, originalTitle); + + int index = 1; + foreach (var streamUrl in streamUrls) + { + if (string.IsNullOrWhiteSpace(streamUrl)) + continue; + + string hostname = StreamDataInvoke.ExtractHostname(streamUrl); + string label = streamUrls.Count > 1 ? $"Джерело {index} ({hostname})" : hostname; + string processedUrl = BuildStreamUrl(init, streamUrl); + tpl.Append(label, processedUrl, subtitles: subs); + index++; + } + + if (tpl.data == null || tpl.data.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + return Content( + rjson ? tpl.ToJson() : tpl.ToHtml(), + rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8" + ); + } + + /// + /// Обробка серіалу: eps структура → сезони → епізоди → play + /// + private async Task HandleSerial(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, string title, string originalTitle, int s, int e, string t, bool rjson) + { + var response = await invoke.GetTvSeries(tmdbId); + if (response?.data?.eps == null || response.data.eps.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + var eps = response.data.eps; + var seasons = eps.Keys + .Select(k => int.TryParse(k, out int sn) ? sn : 0) + .Where(sn => sn > 0) + .OrderBy(sn => sn) + .ToList(); + + if (seasons.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + // Якщо сезон не вибрано — показуємо список сезонів + if (s <= 0) + { + var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); + var seasonTpl = new SeasonTpl(seasons.Count); + foreach (var season in seasons) + { + string seasonLink = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={season}&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}"; + seasonTpl.Append($"Сезон {season}", seasonLink, season.ToString()); + } + + return Content( + rjson ? seasonTpl.ToJson() : seasonTpl.ToHtml(), + rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8" + ); + } + + // Якщо вибрано сезон але не вибрано епізод — показуємо епізоди + string seasonKey = s.ToString(); + if (!eps.ContainsKey(seasonKey) || eps[seasonKey] == null || eps[seasonKey].Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + if (e <= 0) + { + var episodes = eps[seasonKey] + .Select(ep => int.TryParse(ep, out int en) ? en : 0) + .Where(en => en > 0) + .OrderBy(en => en) + .ToList(); + + if (episodes.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); + var episodeTpl = new EpisodeTpl(episodes.Count); + + foreach (var episode in episodes) + { + string episodeName = $"Епізод {episode}"; + string callUrl = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={s}&e={episode}&play=true&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}"; + episodeTpl.Append(episodeName, displayTitle, s.ToString(), episode.ToString("D2"), accsArgs(callUrl), "call"); + } + + return Content( + rjson ? episodeTpl.ToJson() : episodeTpl.ToHtml(), + rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8" + ); + } + + // Якщо є play=true з s та e — це вже оброблено вище в play-гілці + // Сюди не дійдемо, але на всяк випадок: + return OnError("lme_streamdata", refresh_proxy: true); + } + + /// + /// Обробка play-запиту: API з season/episode → JSON зі стрімом + /// + private async Task HandlePlay(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, int season, int episode, string title, string originalTitle) + { + if (tmdbId <= 0 || season <= 0 || episode <= 0) + return OnError("lme_streamdata", refresh_proxy: true); + + var response = await invoke.GetEpisode(tmdbId, season, episode); + if (response?.data?.stream_urls == null || response.data.stream_urls.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + var firstUrl = response.data.stream_urls.FirstOrDefault(u => !string.IsNullOrWhiteSpace(u)); + if (string.IsNullOrEmpty(firstUrl)) + return OnError("lme_streamdata", refresh_proxy: true); + + var subs = CollectSubtitles(response); + string displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); + string streamUrl = BuildStreamUrl(init, firstUrl); + + return UpdateService.Validate(Content( + VideoTpl.ToJson("play", streamUrl, displayTitle, subtitles: subs), + "application/json; charset=utf-8" + )); + } + + /// + /// Зібрати субтитри з відповіді API + /// + private SubtitleTpl CollectSubtitles(StreamDataResponse response) + { + if (response?.default_subs == null || response.default_subs.Count == 0) + return null; + + var tpl = new SubtitleTpl(response.default_subs.Count); + foreach (var sub in response.default_subs) + { + if (!string.IsNullOrWhiteSpace(sub?.url) && !string.IsNullOrWhiteSpace(sub?.lang)) + { + tpl.Append(sub.lang, sub.url); + } + } + + return tpl; + } + + string BuildStreamUrl(OnlinesSettings init, string streamLink) + { + string link = StripLampacArgs(streamLink?.Trim()); + if (string.IsNullOrEmpty(link)) + return link; + + if (ApnHelper.IsEnabled(init)) + { + if (ModInit.ApnHostProvided) + return ApnHelper.WrapUrl(init, link); + + var noApn = (OnlinesSettings)init.Clone(); + noApn.apnstream = false; + noApn.apn = null; + return HostStreamProxy(noApn, link); + } + + return HostStreamProxy(init, link); + } + + private static string StripLampacArgs(string url) + { + if (string.IsNullOrEmpty(url)) + return url; + + string cleaned = System.Text.RegularExpressions.Regex.Replace( + url, + @"([?&])(account_email|uid|nws_id)=[^&]*", + "$1", + System.Text.RegularExpressions.RegexOptions.IgnoreCase + ); + + cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); + return cleaned; + } + + private static bool IsCheckOnlineSearchEnabled() + { + try + { + var onlineType = Type.GetType("Online.ModInit"); + if (onlineType == null) + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + onlineType = asm.GetType("Online.ModInit"); + if (onlineType != null) + break; + } + } + var confField = onlineType?.GetField("conf", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + var conf = confField?.GetValue(null); + var checkProp = conf?.GetType().GetProperty("checkOnlineSearch", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + + if (checkProp?.GetValue(conf) is bool enabled) + return enabled; + } + catch + { + } + + return true; + } + + private static void OnLog(string message) + { + System.Console.WriteLine(message); + } + } +} diff --git a/LME.StreamData/ModInit.cs b/LME.StreamData/ModInit.cs new file mode 100644 index 0000000..50921a1 --- /dev/null +++ b/LME.StreamData/ModInit.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Models.Module; +using Shared.Models.Module.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Shared.Models; +using Shared.Models.Events; +using Shared.Models.Online.Settings; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LME.StreamData +{ + public class ModInit : IModuleLoaded + { + public static double Version => 1.0; + + public static OnlinesSettings StreamDataSettings; + + public static OnlinesSettings Settings + { + get => StreamDataSettings; + set => StreamDataSettings = value; + } + + /// + /// Модуль завантажено + /// + public void Loaded(InitspaceModel initspace) + { + StreamDataSettings = new OnlinesSettings("LME.StreamData", "https://streamdata.vaplayer.ru", streamproxy: false, useproxy: false) + { + displayname = "StreamData", + displayindex = 0, + proxy = new Shared.Models.Base.ProxySettings() + { + useAuth = true, + username = "", + password = "", + list = new string[] { "socks5://ip:port" } + } + }; + + var defaults = JObject.FromObject(StreamDataSettings); + defaults["enabled"] = true; + var conf = ModuleInvoke.Init("LME.StreamData", defaults); + bool hasApn = ApnHelper.TryGetInitConf(conf, out bool apnEnabled, out string apnHost); + conf.Remove("apn"); + conf.Remove("apn_host"); + StreamDataSettings = conf.ToObject(); + if (hasApn) + ApnHelper.ApplyInitConf(apnEnabled, apnHost, StreamDataSettings, useDefaultHostWhenEmpty: true); + + if (hasApn && apnEnabled) + { + StreamDataSettings.streamproxy = false; + } + else if (StreamDataSettings.streamproxy) + { + StreamDataSettings.apnstream = false; + StreamDataSettings.apn = null; + } + + // Реєструємо плагін без пошуку — працюємо тільки через TMDB ID + OnlineRegistry.RegisterWithSearch("lme_streamdata"); + } + + public void Dispose() + { + } + } + + public static class UpdateService + { + private static readonly ModuleUpdateService _service = new( + () => ModInit.Settings?.plugin, + () => ModInit.Version); + + public static Task ConnectAsync(string host, CancellationToken cancellationToken = default) + => _service.ConnectAsync(host, cancellationToken); + + public static bool IsDisconnected() + => _service.IsDisconnected(); + + public static ActionResult Validate(ActionResult result) + => _service.Validate(result); + } +} diff --git a/LME.StreamData/Models/StreamDataModels.cs b/LME.StreamData/Models/StreamDataModels.cs new file mode 100644 index 0000000..8dcbd05 --- /dev/null +++ b/LME.StreamData/Models/StreamDataModels.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace LME.StreamData.Models +{ + public class SubtitleItem + { + public string lang { get; set; } + public string code { get; set; } + public string url { get; set; } + } + + public class StreamDataResponse + { + public string status_code { get; set; } + public StreamDataInfo data { get; set; } + public List default_subs { get; set; } + } + + public class StreamDataInfo + { + public string title { get; set; } + public string imdb_id { get; set; } + public int season { get; set; } + public string episode { get; set; } + public Dictionary> eps { get; set; } + public string file_name { get; set; } + public string backdrop { get; set; } + public List stream_urls { get; set; } + public string thumbnails_url { get; set; } + } +} diff --git a/LME.StreamData/OnlineApi.cs b/LME.StreamData/OnlineApi.cs new file mode 100644 index 0000000..eb4c2d8 --- /dev/null +++ b/LME.StreamData/OnlineApi.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using Shared.Models; +using Shared.Models.Module; +using Shared.Models.Module.Interfaces; +using System.Collections.Generic; + +namespace LME.StreamData +{ + public class OnlineApi : IModuleOnline + { + public List Invoke(HttpContext httpContext, RequestModel requestInfo, string host, OnlineEventsModel args) + { + var online = new List(); + + var init = ModInit.StreamDataSettings; + if (init.enable && !init.rip) + { + if (UpdateService.IsDisconnected()) + init.overridehost = null; + + // StreamData працює з TMDB ID — показуємо для всього контенту + online.Add(new ModuleOnlineItem(init, "lme_streamdata")); + } + + return online; + } + } +} diff --git a/LME.StreamData/StreamData.csproj b/LME.StreamData/StreamData.csproj new file mode 100644 index 0000000..c280999 --- /dev/null +++ b/LME.StreamData/StreamData.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + library + true + + + + + ..\..\Shared.dll + + + + diff --git a/LME.StreamData/StreamDataInvoke.cs b/LME.StreamData/StreamDataInvoke.cs new file mode 100644 index 0000000..7531746 --- /dev/null +++ b/LME.StreamData/StreamDataInvoke.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Web; +using LME.StreamData.Models; +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Online.Settings; + +namespace LME.StreamData +{ + public class StreamDataInvoke + { + private readonly OnlinesSettings _init; + private readonly IHybridCache _hybridCache; + private readonly Action _onLog; + private readonly ProxyManager _proxyManager; + private readonly HttpHydra _httpHydra; + + private const string API_BASE = "https://streamdata.vaplayer.ru/api.php"; + + public StreamDataInvoke(OnlinesSettings init, IHybridCache hybridCache, Action onLog, ProxyManager proxyManager, HttpHydra httpHydra = null) + { + _init = init; + _hybridCache = hybridCache; + _onLog = onLog; + _proxyManager = proxyManager; + _httpHydra = httpHydra; + } + + /// + /// Отримати дані для фільму за TMDB ID + /// + public async Task GetMovie(long tmdbId) + { + string memKey = $"StreamData:movie:{tmdbId}"; + if (_hybridCache.TryGetValue(memKey, out StreamDataResponse cached)) + return cached; + + try + { + string url = $"{API_BASE}?tmdb={tmdbId}&type=movie"; + _onLog?.Invoke($"StreamData movie: {url}"); + + string json = await ApiGet(url); + if (string.IsNullOrEmpty(json)) + return null; + + var response = JsonConvert.DeserializeObject(json); + if (response?.status_code != "200" || response?.data?.stream_urls == null || response.data.stream_urls.Count == 0) + return null; + + _hybridCache.Set(memKey, response, cacheTime(30, init: _init)); + return response; + } + catch (Exception ex) + { + _onLog?.Invoke($"StreamData movie error: {ex.Message}"); + return null; + } + } + + /// + /// Отримати дані для серіалу (без season/episode - отримуємо eps структуру + S01E01) + /// + public async Task GetTvSeries(long tmdbId) + { + string memKey = $"StreamData:tv:{tmdbId}"; + if (_hybridCache.TryGetValue(memKey, out StreamDataResponse cached)) + return cached; + + try + { + string url = $"{API_BASE}?tmdb={tmdbId}&type=tv"; + _onLog?.Invoke($"StreamData tv: {url}"); + + string json = await ApiGet(url); + if (string.IsNullOrEmpty(json)) + return null; + + var response = JsonConvert.DeserializeObject(json); + if (response?.status_code != "200" || response?.data?.eps == null || response.data.eps.Count == 0) + return null; + + _hybridCache.Set(memKey, response, cacheTime(30, init: _init)); + return response; + } + catch (Exception ex) + { + _onLog?.Invoke($"StreamData tv error: {ex.Message}"); + return null; + } + } + + /// + /// Отримати стріми для конкретного епізоду + /// + public async Task GetEpisode(long tmdbId, int season, int episode) + { + string memKey = $"StreamData:ep:{tmdbId}:s{season}e{episode}"; + if (_hybridCache.TryGetValue(memKey, out StreamDataResponse cached)) + return cached; + + try + { + string url = $"{API_BASE}?tmdb={tmdbId}&type=tv&season={season}&episode={episode}"; + _onLog?.Invoke($"StreamData episode: {url}"); + + string json = await ApiGet(url); + if (string.IsNullOrEmpty(json)) + return null; + + var response = JsonConvert.DeserializeObject(json); + if (response?.status_code != "200" || response?.data?.stream_urls == null || response.data.stream_urls.Count == 0) + return null; + + _hybridCache.Set(memKey, response, cacheTime(30, init: _init)); + return response; + } + catch (Exception ex) + { + _onLog?.Invoke($"StreamData episode error: {ex.Message}"); + return null; + } + } + + private Task ApiGet(string url) + { + var headers = new List() + { + new HeadersModel("User-Agent", "Mozilla/5.0"), + new HeadersModel("Referer", "https://brightpathsignals.com/"), + new HeadersModel("X-Requested-With", "XMLHttpRequest") + }; + + if (_httpHydra != null) + return _httpHydra.Get(url, newheaders: headers); + + return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get()); + } + + /// + /// Витягнути назву хоста з URL для відображення джерела + /// + public static string ExtractHostname(string url) + { + if (string.IsNullOrEmpty(url)) + return "невідомо"; + + try + { + var uri = new Uri(url); + return uri.Host; + } + catch + { + return "невідомо"; + } + } + + public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) + { + if (init != null && init.rhub && rhub != -1) + return TimeSpan.FromMinutes(rhub); + + int ctime = init != null && init.cache_time > 0 ? init.cache_time : multiaccess; + if (ctime > multiaccess) + ctime = multiaccess; + + return TimeSpan.FromMinutes(ctime); + } + } +} diff --git a/LME.StreamData/manifest.json b/LME.StreamData/manifest.json new file mode 100644 index 0000000..7b2fd84 --- /dev/null +++ b/LME.StreamData/manifest.json @@ -0,0 +1,12 @@ +{ + "enable": true, + "version": 3, + "initspace": "LME.StreamData.ModInit", + "online": "LME.StreamData.OnlineApi", + "syntaxPaths": [ + "../LME.Shared/GlobalUsings.cs", + "../LME.Shared/Online/OnlineRegistry.cs", + "../LME.Shared/Update/ModuleUpdateService.cs", + "../LME.Shared/Apn/ApnHelper.cs" + ] +} From d2e7f9aa6be82e6abcb50d9e03f355c098eff0a0 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 15 May 2026 22:15:46 +0300 Subject: [PATCH 2/3] fix(streamdata): correctly track explicit APN host configuration Record whether an APN host was explicitly provided during module init by setting `ApnHostProvided` only when APN is enabled and host is non-empty. This preserves accurate APN state for downstream logic that depends on distinguishing defaulted host values from user-provided configuration. --- LME.StreamData/ModInit.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LME.StreamData/ModInit.cs b/LME.StreamData/ModInit.cs index 50921a1..8d731b5 100644 --- a/LME.StreamData/ModInit.cs +++ b/LME.StreamData/ModInit.cs @@ -15,9 +15,10 @@ namespace LME.StreamData { public class ModInit : IModuleLoaded { - public static double Version => 1.0; + public static double Version => 1.5; public static OnlinesSettings StreamDataSettings; + public static bool ApnHostProvided; public static OnlinesSettings Settings { @@ -52,7 +53,7 @@ namespace LME.StreamData StreamDataSettings = conf.ToObject(); if (hasApn) ApnHelper.ApplyInitConf(apnEnabled, apnHost, StreamDataSettings, useDefaultHostWhenEmpty: true); - + ApnHostProvided = hasApn && apnEnabled && !string.IsNullOrWhiteSpace(apnHost); if (hasApn && apnEnabled) { StreamDataSettings.streamproxy = false; From b38c8bd4f45cf77f9ea6b47c5dc48009f4eacf87 Mon Sep 17 00:00:00 2001 From: Felix Date: Fri, 15 May 2026 22:22:19 +0300 Subject: [PATCH 3/3] refactor(streamdata): simplify source labeling and remove hostname parsing Replace hostname-based source labels with stable numbered labels to keep episode and source selection consistent across serial and movie flows. Drop unused URL hostname extraction logic from `StreamDataInvoke` and propagate the selected source parameter through play handling to align with the new voice-tab source selection structure. --- LME.StreamData/Controller.cs | 90 +++++++++++++++++------------- LME.StreamData/StreamDataInvoke.cs | 20 ------- 2 files changed, 51 insertions(+), 59 deletions(-) diff --git a/LME.StreamData/Controller.cs b/LME.StreamData/Controller.cs index 80ffaad..27970d7 100644 --- a/LME.StreamData/Controller.cs +++ b/LME.StreamData/Controller.cs @@ -53,7 +53,7 @@ namespace LME.StreamData.Controllers // play — повернути стрім для конкретного епізоду (call метод) if (play) { - return await HandlePlay(invoke, init, id, s, e, title, original_title); + return await HandlePlay(invoke, init, id, s, e, title, original_title, t); } // Фільм @@ -87,8 +87,7 @@ namespace LME.StreamData.Controllers if (string.IsNullOrWhiteSpace(streamUrl)) continue; - string hostname = StreamDataInvoke.ExtractHostname(streamUrl); - string label = streamUrls.Count > 1 ? $"Джерело {index} ({hostname})" : hostname; + string label = $"Джерело #{index}"; string processedUrl = BuildStreamUrl(init, streamUrl); tpl.Append(label, processedUrl, subtitles: subs); index++; @@ -104,7 +103,7 @@ namespace LME.StreamData.Controllers } /// - /// Обробка серіалу: eps структура → сезони → епізоди → play + /// Обробка серіалу: eps → сезони → епізоди з voice-вкладками (джерела) /// private async Task HandleSerial(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, string title, string originalTitle, int s, int e, string t, bool rjson) { @@ -122,10 +121,15 @@ namespace LME.StreamData.Controllers if (seasons.Count == 0) return OnError("lme_streamdata", refresh_proxy: true); - // Якщо сезон не вибрано — показуємо список сезонів + // Кількість джерел (CDN) з першого запиту + var sources = response.data?.stream_urls?.Where(u => !string.IsNullOrWhiteSpace(u)).ToList() ?? new List(); + int sourceCount = Math.Max(1, sources.Count); + + var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); + + // Список сезонів if (s <= 0) { - var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); var seasonTpl = new SeasonTpl(seasons.Count); foreach (var season in seasons) { @@ -139,47 +143,52 @@ namespace LME.StreamData.Controllers ); } - // Якщо вибрано сезон але не вибрано епізод — показуємо епізоди + // Список епізодів з voice-вкладками для вибору джерела string seasonKey = s.ToString(); if (!eps.ContainsKey(seasonKey) || eps[seasonKey] == null || eps[seasonKey].Count == 0) return OnError("lme_streamdata", refresh_proxy: true); - if (e <= 0) + var episodeNumbers = eps[seasonKey] + .Select(ep => int.TryParse(ep, out int en) ? en : 0) + .Where(en => en > 0) + .OrderBy(en => en) + .ToList(); + + if (episodeNumbers.Count == 0) + return OnError("lme_streamdata", refresh_proxy: true); + + // Voice-вкладки: кожне джерело як окрема "озвучка" + string selectedSource = string.IsNullOrEmpty(t) ? "1" : t; + int selectedIndex = int.TryParse(selectedSource, out int si) && si >= 1 && si <= sourceCount ? si : 1; + + var voiceTpl = new VoiceTpl(); + for (int i = 1; i <= sourceCount; i++) { - var episodes = eps[seasonKey] - .Select(ep => int.TryParse(ep, out int en) ? en : 0) - .Where(en => en > 0) - .OrderBy(en => en) - .ToList(); - - if (episodes.Count == 0) - return OnError("lme_streamdata", refresh_proxy: true); - - var displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); - var episodeTpl = new EpisodeTpl(episodes.Count); - - foreach (var episode in episodes) - { - string episodeName = $"Епізод {episode}"; - string callUrl = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={s}&e={episode}&play=true&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}"; - episodeTpl.Append(episodeName, displayTitle, s.ToString(), episode.ToString("D2"), accsArgs(callUrl), "call"); - } - - return Content( - rjson ? episodeTpl.ToJson() : episodeTpl.ToHtml(), - rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8" - ); + string voiceLink = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={s}&t={i}&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}"; + voiceTpl.Append($"Джерело #{i}", i == selectedIndex, voiceLink); } - // Якщо є play=true з s та e — це вже оброблено вище в play-гілці - // Сюди не дійдемо, але на всяк випадок: - return OnError("lme_streamdata", refresh_proxy: true); + // Епізоди з посиланнями на вибране джерело + var episodeTpl = new EpisodeTpl(episodeNumbers.Count); + foreach (var epNum in episodeNumbers) + { + string episodeName = $"Епізод {epNum}"; + string callUrl = $"{host}/lite/lme_streamdata?id={tmdbId}&serial=1&s={s}&e={epNum}&play=true&t={selectedSource}&title={HttpUtility.UrlEncode(displayTitle)}&original_title={HttpUtility.UrlEncode(originalTitle)}"; + episodeTpl.Append(episodeName, displayTitle, s.ToString(), epNum.ToString("D2"), accsArgs(callUrl), "call"); + } + + episodeTpl.Append(voiceTpl); + + return Content( + rjson ? episodeTpl.ToJson() : episodeTpl.ToHtml(), + rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8" + ); } /// - /// Обробка play-запиту: API з season/episode → JSON зі стрімом + /// Обробка play-запиту: API з season/episode → JSON з вибраним джерелом /// - private async Task HandlePlay(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, int season, int episode, string title, string originalTitle) + private async Task HandlePlay(StreamDataInvoke invoke, OnlinesSettings init, long tmdbId, int season, int episode, string title, string originalTitle, string t) { if (tmdbId <= 0 || season <= 0 || episode <= 0) return OnError("lme_streamdata", refresh_proxy: true); @@ -188,13 +197,16 @@ namespace LME.StreamData.Controllers if (response?.data?.stream_urls == null || response.data.stream_urls.Count == 0) return OnError("lme_streamdata", refresh_proxy: true); - var firstUrl = response.data.stream_urls.FirstOrDefault(u => !string.IsNullOrWhiteSpace(u)); - if (string.IsNullOrEmpty(firstUrl)) + var streamUrls = response.data.stream_urls.Where(u => !string.IsNullOrWhiteSpace(u)).ToList(); + if (streamUrls.Count == 0) return OnError("lme_streamdata", refresh_proxy: true); + // Вибираємо джерело за індексом з voice-вкладки t (1-based) + int sourceIndex = int.TryParse(t, out int si) && si >= 1 && si <= streamUrls.Count ? si - 1 : 0; + string streamUrl = BuildStreamUrl(init, streamUrls[sourceIndex]); + var subs = CollectSubtitles(response); string displayTitle = !string.IsNullOrEmpty(title) ? title : (!string.IsNullOrEmpty(originalTitle) ? originalTitle : response.data.title); - string streamUrl = BuildStreamUrl(init, firstUrl); return UpdateService.Validate(Content( VideoTpl.ToJson("play", streamUrl, displayTitle, subtitles: subs), diff --git a/LME.StreamData/StreamDataInvoke.cs b/LME.StreamData/StreamDataInvoke.cs index 7531746..432b96e 100644 --- a/LME.StreamData/StreamDataInvoke.cs +++ b/LME.StreamData/StreamDataInvoke.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using System.Web; using LME.StreamData.Models; using Newtonsoft.Json; using Shared; @@ -141,25 +140,6 @@ namespace LME.StreamData return Http.Get(_init.cors(url), headers: headers, proxy: _proxyManager.Get()); } - /// - /// Витягнути назву хоста з URL для відображення джерела - /// - public static string ExtractHostname(string url) - { - if (string.IsNullOrEmpty(url)) - return "невідомо"; - - try - { - var uri = new Uri(url); - return uri.Host; - } - catch - { - return "невідомо"; - } - } - public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) { if (init != null && init.rhub && rhub != -1)