Compare commits

..

3 Commits

Author SHA1 Message Date
Felix
0f6b048545 fix(controller): adjust continue statement placement in stream loop
Move the continue statement outside the conditional block to ensure
correct loop control flow. The previous nesting could cause unintended
skipping or processing of stream iterations based on condition evaluation,
potentially leading to logic errors in stream handling.
2026-05-02 16:11:21 +03:00
Felix
b00795c464 refactor(controller): restructure Ashdi stream handling logic
Move the foreach loop inside the null/empty check for ashdiStreams to ensure
proper iteration only when streams are available. This improves code readability
and prevents potential issues with iterating over null or empty collections.
2026-05-02 16:08:27 +03:00
Felix
04bb7d48b5 Усі модулі тепер коректно передають об'єкт SubtitleTpl у шаблони Lampac, що дозволяє відображати субтитри в інтерфейсі плеєра. 2026-05-02 15:54:56 +03:00
15 changed files with 186 additions and 62 deletions

3
.gitignore vendored
View File

@ -16,4 +16,5 @@ bin
obj
.vscode/settings.json
.qwen
log
log
.kilo

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models;
using Shared.Models.Templates;
using System.Text.Json;
using System.Linq;
using System.Text;
@ -14,6 +15,13 @@ using Shared.Engine;
namespace LME.AnimeON
{
public class AshdiStream
{
public string Title { get; set; }
public string Link { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
public class AnimeONInvoke
{
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
@ -192,12 +200,12 @@ namespace LME.AnimeON
public async Task<string> ParseAshdiPage(string url, bool disableAshdiMultivoiceForVod = false)
{
var streams = await ParseAshdiPageStreams(url, disableAshdiMultivoiceForVod);
return streams?.FirstOrDefault().link;
return streams?.FirstOrDefault()?.Link;
}
public async Task<List<(string title, string link)>> ParseAshdiPageStreams(string url, bool disableAshdiMultivoiceForVod = false)
public async Task<List<AshdiStream>> ParseAshdiPageStreams(string url, bool disableAshdiMultivoiceForVod = false)
{
var streams = new List<(string title, string link)>();
var streams = new List<AshdiStream>();
try
{
var headers = new List<HeadersModel>()
@ -234,7 +242,12 @@ namespace LME.AnimeON
continue;
string rawTitle = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null;
streams.Add((BuildDisplayTitle(rawTitle, file, index), file));
streams.Add(new AshdiStream
{
Title = BuildDisplayTitle(rawTitle, file, index),
Link = file,
Subtitles = ApnHelper.ParseSubtitles(item.TryGetProperty("subtitle", out var subtitleProp) ? subtitleProp.GetString() : null)
});
index++;
}
@ -247,7 +260,12 @@ namespace LME.AnimeON
if (match.Success)
{
string file = match.Groups[1].Value;
streams.Add((BuildDisplayTitle("Основне джерело", file, 1), file));
streams.Add(new AshdiStream
{
Title = BuildDisplayTitle("Основне джерело", file, 1),
Link = file,
Subtitles = ApnHelper.ParseSubtitles(ApnHelper.ExtractPlayerSubtitle(html))
});
}
}
catch (Exception ex)

View File

@ -251,12 +251,14 @@ namespace LME.AnimeON.Controllers
{
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{translationName} {ashdiStream.title}";
string callUrl = $"{host}/lite/lme_animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
string optionName = $"{translationName} {ashdiStream.Title}";
string subtitlesParam = ashdiStream.Subtitles != null ? $"&subtitles={HttpUtility.UrlEncode(JsonSerializer.Serialize(ashdiStream.Subtitles.ToObject()))}" : string.Empty;
string callUrl = $"{host}/lite/lme_animeon/play?url={HttpUtility.UrlEncode(ashdiStream.Link)}{subtitlesParam}";
tpl.Append(optionName, accsArgs(callUrl), "call");
}
continue;
}
continue;
}
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
@ -375,7 +377,7 @@ namespace LME.AnimeON.Controllers
}
[HttpGet("lite/lme_animeon/play")]
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null, int serial = 0)
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null, int serial = 0, string subtitles = null)
{
await UpdateService.ConnectAsync(host);
@ -421,10 +423,25 @@ namespace LME.AnimeON.Controllers
forceProxy = true;
}
SubtitleTpl subtitleTpl = null;
if (!string.IsNullOrEmpty(subtitles))
{
try
{
var subtitleDtos = JsonSerializer.Deserialize<List<SubtitleDto>>(subtitles);
if (subtitleDtos != null && subtitleDtos.Count > 0)
{
subtitleTpl = new SubtitleTpl(subtitleDtos.Count);
foreach (var sub in subtitleDtos)
subtitleTpl.Append(sub.label, sub.url);
}
}
catch { }
}
string streamUrl = BuildStreamUrl(init, streamLink, streamHeaders, forceProxy);
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? string.Empty}\"}}";
OnLog("AnimeON Play: return call JSON");
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? string.Empty, subtitles: subtitleTpl), "application/json; charset=utf-8"));
}
private static string StripLampacArgs(string url)

View File

@ -166,7 +166,7 @@ namespace LME.KlonFUN.Controllers
: $"Серія {episode.Number}";
string streamUrl = BuildStreamUrl(init, episode.Link);
episodeTpl.Append(episodeTitle, contentTitle, s.ToString(), episode.Number.ToString("D2"), streamUrl);
episodeTpl.Append(episodeTitle, contentTitle, s.ToString(), episode.Number.ToString("D2"), streamUrl, subtitles: episode.Subtitles);
}
episodeTpl.Append(voiceTpl);
@ -190,7 +190,7 @@ namespace LME.KlonFUN.Controllers
: $"Варіант {i + 1}";
string streamUrl = BuildStreamUrl(init, stream.Link);
movieTpl.Append(label, streamUrl);
movieTpl.Append(label, streamUrl, subtitles: stream.Subtitles);
}
return rjson

View File

@ -206,7 +206,8 @@ namespace LME.KlonFUN
streams.Add(new MovieStream
{
Title = voiceTitle,
Link = link
Link = link,
Subtitles = ApnHelper.ParseSubtitles(item.Value<string>("subtitle"))
});
index++;
@ -221,7 +222,8 @@ namespace LME.KlonFUN
streams.Add(new MovieStream
{
Title = FormatMovieTitle("Основне джерело", directMatch.Groups["url"].Value, 1),
Link = directMatch.Groups["url"].Value
Link = directMatch.Groups["url"].Value,
Subtitles = ApnHelper.ParseSubtitles(ApnHelper.ExtractPlayerSubtitle(playerHtml))
});
}
}
@ -310,7 +312,8 @@ namespace LME.KlonFUN
{
Number = episodeNumber,
Title = string.IsNullOrWhiteSpace(episodeTitle) ? $"Серія {episodeNumber}" : episodeTitle,
Link = link
Link = link,
Subtitles = ApnHelper.ParseSubtitles(episodeObj.Value<string>("subtitle"))
});
episodeFallback++;

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Shared.Models.Templates;
namespace LME.KlonFUN.Models
{
@ -47,6 +48,7 @@ namespace LME.KlonFUN.Models
{
public string Title { get; set; }
public string Link { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
public class SerialEpisode
@ -54,6 +56,7 @@ namespace LME.KlonFUN.Models
public int Number { get; set; }
public string Title { get; set; }
public string Link { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
public class SerialVoice

View File

@ -107,7 +107,7 @@ namespace LME.Makhno
if (play)
return UpdateService.Validate(Redirect(streamUrl));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, episodeTitle), "application/json; charset=utf-8"));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, episodeTitle, subtitles: episode.Subtitles), "application/json; charset=utf-8"));
}
}
@ -150,7 +150,7 @@ namespace LME.Makhno
if (play)
return UpdateService.Validate(Redirect(streamUrl));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title), "application/json; charset=utf-8"));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title, subtitles: playerData.Subtitles ?? playerData.Movies?.FirstOrDefault(m => m.File == playerData.File)?.Subtitles), "application/json; charset=utf-8"));
}
private async Task<ActionResult> HandleMovie(string playUrl, string imdb_id, string title, string original_title, int year, bool rjson, MakhnoInvoke invoke)
@ -171,7 +171,8 @@ namespace LME.Makhno
{
File = playerData.File,
Title = "Основне джерело",
Quality = "auto"
Quality = "auto",
Subtitles = playerData.Subtitles
});
}
@ -189,7 +190,7 @@ namespace LME.Makhno
? stream.Title
: $"Варіант {index}";
tpl.Append(label, BuildStreamUrl(init, stream.File));
tpl.Append(label, BuildStreamUrl(init, stream.File), subtitles: stream.Subtitles);
index++;
}
@ -390,13 +391,15 @@ namespace LME.Makhno
if (!string.IsNullOrEmpty(episode.File))
{
string streamUrl = BuildStreamUrl(init, episode.File);
episode_tpl.Append(
episode.Title,
title ?? original_title,
requestedSeason.ToString(),
(i + 1).ToString("D2"),
streamUrl
);
episode_tpl.Append(
episode.Title,
title ?? original_title,
requestedSeason.ToString(),
(i + 1).ToString("D2"),
streamUrl,
subtitles: episode.Subtitles
);
}
}
}

View File

@ -114,18 +114,22 @@ namespace LME.Makhno
{
string file = fileMatch.Groups[1].Value;
var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
var subtitles = ApnHelper.ParseSubtitles(ApnHelper.ExtractPlayerSubtitle(html));
return new PlayerData
{
File = file,
Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null,
Voices = new List<Voice>(),
Subtitles = subtitles,
Movies = new List<MovieVariant>()
{
new MovieVariant
{
File = file,
Quality = DetectQualityTag(file) ?? "auto",
Title = BuildMovieTitle("Основне джерело", file, 1)
Title = BuildMovieTitle("Основне джерело", file, 1),
Subtitles = subtitles
}
}
};
@ -238,7 +242,8 @@ namespace LME.Makhno
Title = episode["title"]?.ToString(),
File = episode["file"]?.ToString(),
Poster = episode["poster"]?.ToString(),
Subtitle = episode["subtitle"]?.ToString()
Subtitle = episode["subtitle"]?.ToString(),
Subtitles = ApnHelper.ParseSubtitles(episode["subtitle"]?.ToString())
});
}
}
@ -289,7 +294,8 @@ namespace LME.Makhno
{
File = file,
Quality = DetectQualityTag($"{rawTitle} {file}") ?? "auto",
Title = BuildMovieTitle(rawTitle, file, index)
Title = BuildMovieTitle(rawTitle, file, index),
Subtitles = ApnHelper.ParseSubtitles(item["subtitle"]?.ToString())
});
index++;
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Shared.Models.Templates;
namespace LME.Makhno.Models
{
@ -9,6 +10,7 @@ namespace LME.Makhno.Models
public List<Voice> Voices { get; set; }
public List<Season> Seasons { get; set; }
public List<MovieVariant> Movies { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
public class Voice
@ -30,6 +32,7 @@ namespace LME.Makhno.Models
public string Id { get; set; }
public string Poster { get; set; }
public string Subtitle { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
public class MovieVariant
@ -37,5 +40,6 @@ namespace LME.Makhno.Models
public string Title { get; set; }
public string File { get; set; }
public string Quality { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
@ -177,12 +178,14 @@ namespace LME.Mikai.Controllers
{
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{voice.DisplayName} {ashdiStream.title}";
string ashdiCallUrl = $"{host}/lite/lme_mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
string optionName = $"{voice.DisplayName} {ashdiStream.Title}";
string subtitlesParam = ashdiStream.Subtitles != null ? $"&subtitles={HttpUtility.UrlEncode(JsonSerializer.Serialize(ashdiStream.Subtitles.ToObject()))}" : string.Empty;
string ashdiCallUrl = $"{host}/lite/lme_mikai/play?url={HttpUtility.UrlEncode(ashdiStream.Link)}&title={HttpUtility.UrlEncode(displayTitle)}{subtitlesParam}";
movieTpl.Append(optionName, accsArgs(ashdiCallUrl), "call");
}
continue;
}
continue;
}
string callUrl = $"{host}/lite/lme_mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
@ -204,7 +207,7 @@ namespace LME.Mikai.Controllers
}
[HttpGet("lite/lme_mikai/play")]
public async Task<ActionResult> Play(string url, string title = null, int serial = 0)
public async Task<ActionResult> Play(string url, string title = null, int serial = 0, string subtitles = null)
{
await UpdateService.ConnectAsync(host);
@ -235,9 +238,24 @@ namespace LME.Mikai.Controllers
forceProxy = true;
}
SubtitleTpl subtitleTpl = null;
if (!string.IsNullOrEmpty(subtitles))
{
try
{
var subtitleDtos = JsonSerializer.Deserialize<List<SubtitleDto>>(subtitles);
if (subtitleDtos != null && subtitleDtos.Count > 0)
{
subtitleTpl = new SubtitleTpl(subtitleDtos.Count);
foreach (var sub in subtitleDtos)
subtitleTpl.Append(sub.label, sub.url);
}
}
catch { }
}
string streamUrl = BuildStreamUrl(init, streamLink, streamHeaders, forceProxy);
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? string.Empty}\"}}";
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? string.Empty, subtitles: subtitleTpl), "application/json; charset=utf-8"));
}
private async Task<List<MikaiAnime>> CollectSeasonDetails(MikaiAnime details, MikaiInvoke invoke)

View File

@ -11,9 +11,17 @@ using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
namespace LME.Mikai
{
public class AshdiStream
{
public string Title { get; set; }
public string Link { get; set; }
public SubtitleTpl Subtitles { get; set; }
}
public class MikaiInvoke
{
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
@ -176,12 +184,12 @@ namespace LME.Mikai
public async Task<string> ParseAshdiPage(string url, bool disableAshdiMultivoiceForVod = false)
{
var streams = await ParseAshdiPageStreams(url, disableAshdiMultivoiceForVod);
return streams?.FirstOrDefault().link;
return streams?.FirstOrDefault()?.Link;
}
public async Task<List<(string title, string link)>> ParseAshdiPageStreams(string url, bool disableAshdiMultivoiceForVod = false)
public async Task<List<AshdiStream>> ParseAshdiPageStreams(string url, bool disableAshdiMultivoiceForVod = false)
{
var streams = new List<(string title, string link)>();
var streams = new List<AshdiStream>();
try
{
var headers = new List<HeadersModel>()
@ -218,7 +226,12 @@ namespace LME.Mikai
continue;
string rawTitle = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null;
streams.Add((BuildDisplayTitle(rawTitle, file, index), file));
streams.Add(new AshdiStream
{
Title = BuildDisplayTitle(rawTitle, file, index),
Link = file,
Subtitles = ApnHelper.ParseSubtitles(item.TryGetProperty("subtitle", out var subtitleProp) ? subtitleProp.GetString() : null)
});
index++;
}
@ -231,7 +244,12 @@ namespace LME.Mikai
if (match.Success)
{
string file = match.Groups[1].Value;
streams.Add((BuildDisplayTitle("Основне джерело", file, 1), file));
streams.Add(new AshdiStream
{
Title = BuildDisplayTitle("Основне джерело", file, 1),
Link = file,
Subtitles = ApnHelper.ParseSubtitles(ApnHelper.ExtractPlayerSubtitle(html))
});
}
}
catch (Exception ex)

View File

@ -1,6 +1,9 @@
using Newtonsoft.Json.Linq;
using Shared.Models.Base;
using Shared.Models.Templates;
using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
namespace Shared.Engine
@ -9,6 +12,8 @@ namespace Shared.Engine
{
public const string DefaultHost = "https://tut.im/proxy.php?url={encodeurl}";
private static readonly Regex SubtitleLineRegex = new Regex(@"\[([^\]]+)\]([^,]+)", RegexOptions.Compiled);
public static bool TryGetInitConf(JObject conf, out bool enabled, out string host)
{
enabled = false;
@ -120,6 +125,40 @@ namespace Shared.Engine
return $"{host.TrimEnd('/')}/{url}";
}
public static SubtitleTpl ParseSubtitles(string subtitleValue)
{
if (string.IsNullOrWhiteSpace(subtitleValue))
return null;
var subtitles = new SubtitleTpl();
string normalized = WebUtility.HtmlDecode(subtitleValue)
.Replace("\\/", "/")
.Replace("\\'", "'")
.Replace("\\\"", "\"");
foreach (Match match in SubtitleLineRegex.Matches(normalized))
{
string label = WebUtility.HtmlDecode(match.Groups[1].Value).Trim();
string url = WebUtility.HtmlDecode(match.Groups[2].Value).Trim();
if (!string.IsNullOrWhiteSpace(label) && !string.IsNullOrWhiteSpace(url))
subtitles.Append(label, url);
}
return subtitles.IsEmpty ? null : subtitles;
}
public static string ExtractPlayerSubtitle(string html)
{
if (string.IsNullOrWhiteSpace(html))
return null;
var match = Regex.Match(html, @"subtitle\s*:\s*['""]([^'""']+)['""]", RegexOptions.IgnoreCase);
if (!match.Success)
match = Regex.Match(html, @"subtitle['""]?\s*:\s*['""]([^'""']+)['""]", RegexOptions.IgnoreCase);
return match.Success ? match.Groups[1].Value : null;
}
private static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))

View File

@ -104,9 +104,10 @@ namespace LME.Uaflix.Controllers
{
// Повертаємо JSON з інформацією про стрім для методу 'play'
string streamUrl = BuildStreamUrl(init, playResult.streams.First().link);
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? original_title}\"}}";
var subtitles = playResult.subtitles ?? playResult.streams.FirstOrDefault(s => s.subtitles != null)?.subtitles;
OnLog($"=== RETURN: call method JSON for episode_url ===");
return UpdateService.Validate(Content(jsonResult, "application/json; charset=utf-8"));
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title, subtitles: subtitles), "application/json; charset=utf-8"));
}
OnLog("=== RETURN: call method no streams ===");
@ -284,7 +285,8 @@ namespace LME.Uaflix.Controllers
e: ep.Number.ToString(),
link: accsArgs(callUrl),
method: "call",
streamlink: accsArgs($"{callUrl}&play=true")
streamlink: accsArgs($"{callUrl}&play=true"),
subtitles: ApnHelper.ParseSubtitles(ep.Subtitle)
);
}
else
@ -296,7 +298,8 @@ namespace LME.Uaflix.Controllers
title: title,
s: s.ToString(),
e: ep.Number.ToString(),
link: playUrl
link: playUrl,
subtitles: ApnHelper.ParseSubtitles(ep.Subtitle)
);
}
@ -351,7 +354,7 @@ namespace LME.Uaflix.Controllers
? stream.title
: $"Варіант {index}";
tpl.Append(label, BuildStreamUrl(init, stream.link));
tpl.Append(label, BuildStreamUrl(init, stream.link), subtitles: stream.subtitles ?? playResult.subtitles);
index++;
}

View File

@ -15,5 +15,6 @@ namespace LME.Uaflix.Models
public string link { get; set; }
public string quality { get; set; }
public string title { get; set; }
public SubtitleTpl? subtitles { get; set; }
}
}

View File

@ -582,7 +582,8 @@ namespace LME.Uaflix
{
link = fileUrl,
quality = DetectQualityTag($"{rawTitle} {fileUrl}") ?? "auto",
title = BuildDisplayTitle(rawTitle, fileUrl, index)
title = BuildDisplayTitle(rawTitle, fileUrl, index),
subtitles = ApnHelper.ParseSubtitles(item?["subtitle"]?.ToString())
});
index++;
}
@ -605,7 +606,8 @@ namespace LME.Uaflix
{
link = fallbackFile,
quality = DetectQualityTag(fallbackFile) ?? "auto",
title = BuildDisplayTitle(ExtractVoiceFromUrl(fallbackFile), fallbackFile, 1)
title = BuildDisplayTitle(ExtractVoiceFromUrl(fallbackFile), fallbackFile, 1),
subtitles = ApnHelper.ParseSubtitles(ApnHelper.ExtractPlayerSubtitle(html))
});
return result;
@ -2050,19 +2052,7 @@ namespace LME.Uaflix
string url = $"https://ashdi.vip/vod/{id}";
var html = await GetHtml(AshdiRequestUrl(url), new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") });
string subtitle = new Regex("subtitle(\")?:\"([^\"]+)\"").Match(html).Groups[2].Value;
if (!string.IsNullOrEmpty(subtitle))
{
var match = new Regex("\\[([^\\]]+)\\](https?://[^\\,]+)").Match(subtitle);
var st = new Shared.Models.Templates.SubtitleTpl();
while (match.Success)
{
st.Append(match.Groups[1].Value, match.Groups[2].Value);
match = match.NextMatch();
}
if (st.data != null && st.data.Count > 0)
return st;
}
return null;
return ApnHelper.ParseSubtitles(subtitle);
}
private static string WithAshdiMultivoice(string url)