Compare commits

..

No commits in common. "4d4ac22601331188fd88c0f1020d914de1ba2a6a" and "104afc463edd364e4737d503db56900ac3d25a5c" have entirely different histories.

20 changed files with 89 additions and 1266 deletions

View File

@ -7,8 +7,6 @@ using Shared.Models;
using System.Text.Json; using System.Text.Json;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using AnimeON.Models; using AnimeON.Models;
using Shared.Engine; using Shared.Engine;
@ -16,9 +14,6 @@ namespace AnimeON
{ {
public class AnimeONInvoke public class AnimeONInvoke
{ {
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
private OnlinesSettings _init; private OnlinesSettings _init;
private IHybridCache _hybridCache; private IHybridCache _hybridCache;
private Action<string> _onLog; private Action<string> _onLog;
@ -189,13 +184,6 @@ namespace AnimeON
public async Task<string> ParseAshdiPage(string url) public async Task<string> ParseAshdiPage(string url)
{ {
var streams = await ParseAshdiPageStreams(url);
return streams?.FirstOrDefault().link;
}
public async Task<List<(string title, string link)>> ParseAshdiPageStreams(string url)
{
var streams = new List<(string title, string link)>();
try try
{ {
var headers = new List<HeadersModel>() var headers = new List<HeadersModel>()
@ -204,48 +192,16 @@ namespace AnimeON
new HeadersModel("Referer", "https://ashdi.vip/") new HeadersModel("Referer", "https://ashdi.vip/")
}; };
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url)); string requestUrl = AshdiRequestUrl(url);
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}"); _onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get()); string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html)) if (string.IsNullOrEmpty(html))
return streams; return null;
string rawArray = ExtractPlayerFileArray(html); var match = System.Text.RegularExpressions.Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
if (!string.IsNullOrWhiteSpace(rawArray))
{
string json = WebUtility.HtmlDecode(rawArray)
.Replace("\\/", "/")
.Replace("\\'", "'")
.Replace("\\\"", "\"");
using var jsonDoc = JsonDocument.Parse(json);
if (jsonDoc.RootElement.ValueKind == JsonValueKind.Array)
{
int index = 1;
foreach (var item in jsonDoc.RootElement.EnumerateArray())
{
if (!item.TryGetProperty("file", out var fileProp))
continue;
string file = fileProp.GetString();
if (string.IsNullOrWhiteSpace(file))
continue;
string rawTitle = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null;
streams.Add((BuildDisplayTitle(rawTitle, file, index), file));
index++;
}
if (streams.Count > 0)
return streams;
}
}
var match = Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
if (match.Success) if (match.Success)
{ {
string file = match.Groups[1].Value; return match.Groups[1].Value;
streams.Add((BuildDisplayTitle("Основне джерело", file, 1), file));
} }
} }
catch (Exception ex) catch (Exception ex)
@ -253,7 +209,7 @@ namespace AnimeON
_onLog($"AnimeON ParseAshdiPage error: {ex.Message}"); _onLog($"AnimeON ParseAshdiPage error: {ex.Message}");
} }
return streams; return null;
} }
public async Task<string> ResolveEpisodeStream(int episodeId) public async Task<string> ResolveEpisodeStream(int episodeId)
@ -304,168 +260,6 @@ namespace AnimeON
return url; return url;
} }
private static string WithAshdiMultivoice(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
return url;
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
return url;
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
}
private static string BuildDisplayTitle(string rawTitle, string link, int index)
{
string normalized = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
string qualityTag = DetectQualityTag($"{normalized} {link}");
if (string.IsNullOrWhiteSpace(qualityTag))
return normalized;
if (normalized.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || normalized.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
return normalized;
return $"{qualityTag} {normalized}";
}
private static string DetectQualityTag(string value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
if (Quality4kRegex.IsMatch(value))
return "[4K]";
if (QualityFhdRegex.IsMatch(value))
return "[FHD]";
return null;
}
private static string StripMoviePrefix(string title)
{
if (string.IsNullOrWhiteSpace(title))
return title;
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
return normalized;
string prefix = normalized.Substring(0, sepIndex).Trim();
string suffix = normalized.Substring(sepIndex + 3).Trim();
if (string.IsNullOrWhiteSpace(suffix))
return normalized;
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
return suffix;
return normalized;
}
private static string ExtractPlayerFileArray(string html)
{
if (string.IsNullOrWhiteSpace(html))
return null;
int searchIndex = 0;
while (searchIndex >= 0 && searchIndex < html.Length)
{
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
if (fileIndex < 0)
return null;
int colonIndex = html.IndexOf(':', fileIndex);
if (colonIndex < 0)
return null;
int startIndex = colonIndex + 1;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
{
startIndex++;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
}
if (startIndex >= html.Length || html[startIndex] != '[')
{
searchIndex = fileIndex + 4;
continue;
}
return ExtractBracketArray(html, startIndex);
}
return null;
}
private static string ExtractBracketArray(string text, int startIndex)
{
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
return null;
int depth = 0;
bool inString = false;
bool escaped = false;
char quoteChar = '\0';
for (int i = startIndex; i < text.Length; i++)
{
char ch = text[i];
if (inString)
{
if (escaped)
{
escaped = false;
continue;
}
if (ch == '\\')
{
escaped = true;
continue;
}
if (ch == quoteChar)
{
inString = false;
quoteChar = '\0';
}
continue;
}
if (ch == '"' || ch == '\'')
{
inString = true;
quoteChar = ch;
continue;
}
if (ch == '[')
{
depth++;
continue;
}
if (ch == ']')
{
depth--;
if (depth == 0)
return text.Substring(startIndex, i - startIndex + 1);
}
}
return null;
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) 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) if (init != null && init.rhub && rhub != -1)

View File

@ -223,21 +223,6 @@ namespace AnimeON.Controllers
string translationName = $"[{player.Name}] {fundub.Fundub.Name}"; string translationName = $"[{player.Name}] {fundub.Fundub.Name}";
bool needsResolve = player.Name?.ToLower() == "moon" || player.Name?.ToLower() == "ashdi"; bool needsResolve = player.Name?.ToLower() == "moon" || player.Name?.ToLower() == "ashdi";
if (streamLink.Contains("ashdi.vip/vod", StringComparison.OrdinalIgnoreCase))
{
var ashdiStreams = await invoke.ParseAshdiPageStreams(streamLink);
if (ashdiStreams != null && ashdiStreams.Count > 0)
{
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{translationName} {ashdiStream.title}";
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(ashdiStream.link)}";
tpl.Append(optionName, accsArgs(callUrl), "call");
}
continue;
}
}
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod")) if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
{ {
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}"; string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";

View File

@ -25,7 +25,7 @@ namespace AnimeON
{ {
public class ModInit public class ModInit
{ {
public static double Version => 3.6; public static double Version => 3.5;
public static OnlinesSettings AnimeON; public static OnlinesSettings AnimeON;
public static bool ApnHostProvided; public static bool ApnHostProvided;

View File

@ -21,8 +21,6 @@ namespace KlonFUN
private static readonly Regex DirectFileRegex = new Regex(@"file\s*:\s*['""](?<url>https?://[^'"">\s]+\.m3u8[^'"">\s]*)['""]", RegexOptions.Singleline | RegexOptions.IgnoreCase); private static readonly Regex DirectFileRegex = new Regex(@"file\s*:\s*['""](?<url>https?://[^'"">\s]+\.m3u8[^'"">\s]*)['""]", RegexOptions.Singleline | RegexOptions.IgnoreCase);
private static readonly Regex YearRegex = new Regex(@"(19|20)\d{2}", RegexOptions.IgnoreCase); private static readonly Regex YearRegex = new Regex(@"(19|20)\d{2}", RegexOptions.IgnoreCase);
private static readonly Regex NumberRegex = new Regex(@"(\d+)", RegexOptions.IgnoreCase); private static readonly Regex NumberRegex = new Regex(@"(\d+)", RegexOptions.IgnoreCase);
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
private readonly OnlinesSettings _init; private readonly OnlinesSettings _init;
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
@ -183,7 +181,7 @@ namespace KlonFUN
try try
{ {
string playerHtml = await GetPlayerHtml(WithAshdiMultivoice(playerUrl)); string playerHtml = await GetPlayerHtml(playerUrl);
if (string.IsNullOrWhiteSpace(playerHtml)) if (string.IsNullOrWhiteSpace(playerHtml))
return null; return null;
@ -199,7 +197,9 @@ namespace KlonFUN
if (string.IsNullOrWhiteSpace(link)) if (string.IsNullOrWhiteSpace(link))
continue; continue;
string voiceTitle = FormatMovieTitle(item.Value<string>("title"), link, index); string voiceTitle = CleanText(item.Value<string>("title"));
if (string.IsNullOrWhiteSpace(voiceTitle))
voiceTitle = $"Варіант {index}";
streams.Add(new MovieStream streams.Add(new MovieStream
{ {
@ -218,7 +218,7 @@ namespace KlonFUN
{ {
streams.Add(new MovieStream streams.Add(new MovieStream
{ {
Title = FormatMovieTitle("Основне джерело", directMatch.Groups["url"].Value, 1), Title = "Основне джерело",
Link = directMatch.Groups["url"].Value Link = directMatch.Groups["url"].Value
}); });
} }
@ -634,71 +634,6 @@ namespace KlonFUN
return $"{baseName} #{count}"; return $"{baseName} #{count}";
} }
private static string WithAshdiMultivoice(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
return url;
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
return url;
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
}
private static string FormatMovieTitle(string rawTitle, string streamUrl, int index)
{
string title = StripMoviePrefix(CleanText(rawTitle));
if (string.IsNullOrWhiteSpace(title))
title = $"Варіант {index}";
string tag = DetectQualityTag($"{title} {streamUrl}");
if (string.IsNullOrWhiteSpace(tag))
return title;
if (title.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || title.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
return title;
return $"{tag} {title}";
}
private static string StripMoviePrefix(string title)
{
if (string.IsNullOrWhiteSpace(title))
return title;
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
return normalized;
string prefix = normalized.Substring(0, sepIndex).Trim();
string suffix = normalized.Substring(sepIndex + 3).Trim();
if (string.IsNullOrWhiteSpace(suffix))
return normalized;
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
return suffix;
return normalized;
}
private static string DetectQualityTag(string source)
{
if (string.IsNullOrWhiteSpace(source))
return null;
if (Quality4kRegex.IsMatch(source))
return "[4K]";
if (QualityFhdRegex.IsMatch(source))
return "[FHD]";
return null;
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) 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) if (init != null && init.rhub && rhub != -1)

View File

@ -18,7 +18,7 @@ namespace KlonFUN
{ {
public class ModInit public class ModInit
{ {
public static double Version => 1.1; public static double Version => 1.0;
public static OnlinesSettings KlonFUN; public static OnlinesSettings KlonFUN;
public static bool ApnHostProvided; public static bool ApnHostProvided;

View File

@ -173,37 +173,15 @@ namespace Makhno
return await invoke.GetPlayerData(playUrl); return await invoke.GetPlayerData(playUrl);
}); });
var movieStreams = playerData?.Movies? if (playerData?.File == null)
.Where(m => m != null && !string.IsNullOrEmpty(m.File))
.ToList() ?? new List<MovieVariant>();
if (movieStreams.Count == 0 && !string.IsNullOrEmpty(playerData?.File))
{
movieStreams.Add(new MovieVariant
{
File = playerData.File,
Title = "Основне джерело",
Quality = "auto"
});
}
if (movieStreams.Count == 0)
{ {
OnLog("Makhno HandleMovie: no file parsed"); OnLog("Makhno HandleMovie: no file parsed");
return OnError(); return OnError();
} }
var tpl = new MovieTpl(title ?? original_title, original_title, movieStreams.Count); string movieLink = $"{host}/makhno/play/movie?imdb_id={HttpUtility.UrlEncode(imdb_id)}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&play=true";
int index = 1; var tpl = new MovieTpl(title ?? original_title, original_title, 1);
foreach (var stream in movieStreams) tpl.Append(title ?? original_title, accsArgs(movieLink), method: "play");
{
string label = !string.IsNullOrWhiteSpace(stream.Title)
? stream.Title
: $"Варіант {index}";
tpl.Append(label, BuildStreamUrl(init, stream.File));
index++;
}
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
} }

View File

@ -19,8 +19,6 @@ namespace Makhno
{ {
private const string WormholeHost = "http://wormhole.lampame.v6.rocks/"; private const string WormholeHost = "http://wormhole.lampame.v6.rocks/";
private const string AshdiHost = "https://ashdi.vip"; private const string AshdiHost = "https://ashdi.vip";
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
private readonly OnlinesSettings _init; private readonly OnlinesSettings _init;
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
@ -203,20 +201,19 @@ namespace Makhno
try try
{ {
string sourceUrl = WithAshdiMultivoice(playerUrl); string requestUrl = playerUrl;
string requestUrl = sourceUrl;
var headers = new List<HeadersModel>() var headers = new List<HeadersModel>()
{ {
new HeadersModel("User-Agent", Http.UserAgent) new HeadersModel("User-Agent", Http.UserAgent)
}; };
if (sourceUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase)) if (playerUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
{ {
headers.Add(new HeadersModel("Referer", "https://ashdi.vip/")); headers.Add(new HeadersModel("Referer", "https://ashdi.vip/"));
} }
if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost)) if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
requestUrl = ApnHelper.WrapUrl(_init, sourceUrl); requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
_onLog($"Makhno getting player data from: {requestUrl}"); _onLog($"Makhno getting player data from: {requestUrl}");
@ -246,22 +243,12 @@ namespace Makhno
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("[")) if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
{ {
string file = fileMatch.Groups[1].Value;
var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase); var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
return new PlayerData return new PlayerData
{ {
File = file, File = fileMatch.Groups[1].Value,
Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null, Poster = posterMatch.Success ? posterMatch.Groups[1].Value : null,
Voices = new List<Voice>(), Voices = new List<Voice>()
Movies = new List<MovieVariant>()
{
new MovieVariant
{
File = file,
Quality = DetectQualityTag(file) ?? "auto",
Title = BuildMovieTitle("Основне джерело", file, 1)
}
}
}; };
} }
@ -273,14 +260,12 @@ namespace Makhno
if (!string.IsNullOrEmpty(jsonData)) if (!string.IsNullOrEmpty(jsonData))
{ {
var voices = ParseVoicesJson(jsonData); var voices = ParseVoicesJson(jsonData);
var movies = ParseMovieVariantsJson(jsonData);
_onLog($"Makhno ParsePlayerData: voices={voices?.Count ?? 0}"); _onLog($"Makhno ParsePlayerData: voices={voices?.Count ?? 0}");
return new PlayerData return new PlayerData
{ {
File = movies.FirstOrDefault()?.File, File = null,
Poster = null, Poster = null,
Voices = voices, Voices = voices
Movies = movies
}; };
} }
@ -292,16 +277,7 @@ namespace Makhno
{ {
File = m3u8Match.Groups[1].Value, File = m3u8Match.Groups[1].Value,
Poster = null, Poster = null,
Voices = new List<Voice>(), Voices = new List<Voice>()
Movies = new List<MovieVariant>()
{
new MovieVariant
{
File = m3u8Match.Groups[1].Value,
Quality = DetectQualityTag(m3u8Match.Groups[1].Value) ?? "auto",
Title = BuildMovieTitle("Основне джерело", m3u8Match.Groups[1].Value, 1)
}
}
}; };
} }
@ -313,16 +289,7 @@ namespace Makhno
{ {
File = sourceMatch.Groups[1].Value, File = sourceMatch.Groups[1].Value,
Poster = null, Poster = null,
Voices = new List<Voice>(), Voices = new List<Voice>()
Movies = new List<MovieVariant>()
{
new MovieVariant
{
File = sourceMatch.Groups[1].Value,
Quality = DetectQualityTag(sourceMatch.Groups[1].Value) ?? "auto",
Title = BuildMovieTitle("Основне джерело", sourceMatch.Groups[1].Value, 1)
}
}
}; };
} }
@ -402,41 +369,6 @@ namespace Makhno
} }
} }
private List<MovieVariant> ParseMovieVariantsJson(string jsonData)
{
try
{
var voicesArray = JsonConvert.DeserializeObject<List<JObject>>(jsonData);
var movies = new List<MovieVariant>();
if (voicesArray == null || voicesArray.Count == 0)
return movies;
int index = 1;
foreach (var item in voicesArray)
{
string file = item?["file"]?.ToString();
if (string.IsNullOrWhiteSpace(file))
continue;
string rawTitle = item["title"]?.ToString();
movies.Add(new MovieVariant
{
File = file,
Quality = DetectQualityTag($"{rawTitle} {file}") ?? "auto",
Title = BuildMovieTitle(rawTitle, file, index)
});
index++;
}
return movies;
}
catch (Exception ex)
{
_onLog($"Makhno ParseMovieVariantsJson error: {ex.Message}");
return new List<MovieVariant>();
}
}
private string ExtractPlayerJson(string html) private string ExtractPlayerJson(string html)
{ {
if (string.IsNullOrEmpty(html)) if (string.IsNullOrEmpty(html))
@ -599,69 +531,6 @@ namespace Makhno
return null; return null;
} }
private static string WithAshdiMultivoice(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
return url;
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
return url;
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
}
private static string BuildMovieTitle(string rawTitle, string file, int index)
{
string title = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
string qualityTag = DetectQualityTag($"{title} {file}");
if (string.IsNullOrWhiteSpace(qualityTag))
return title;
if (title.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || title.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
return title;
return $"{qualityTag} {title}";
}
private static string DetectQualityTag(string value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
if (Quality4kRegex.IsMatch(value))
return "[4K]";
if (QualityFhdRegex.IsMatch(value))
return "[FHD]";
return null;
}
private static string StripMoviePrefix(string title)
{
if (string.IsNullOrWhiteSpace(title))
return title;
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
return normalized;
string prefix = normalized.Substring(0, sepIndex).Trim();
string suffix = normalized.Substring(sepIndex + 3).Trim();
if (string.IsNullOrWhiteSpace(suffix))
return normalized;
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
return suffix;
return normalized;
}
public string ExtractAshdiPath(string value) public string ExtractAshdiPath(string value)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))

View File

@ -23,7 +23,7 @@ namespace Makhno
{ {
public class ModInit public class ModInit
{ {
public static double Version => 2.0; public static double Version => 1.9;
public static OnlinesSettings Makhno; public static OnlinesSettings Makhno;
public static bool ApnHostProvided; public static bool ApnHostProvided;

View File

@ -36,7 +36,6 @@ namespace Makhno.Models
public string Poster { get; set; } public string Poster { get; set; }
public List<Voice> Voices { get; set; } public List<Voice> Voices { get; set; }
public List<Season> Seasons { get; set; } public List<Season> Seasons { get; set; }
public List<MovieVariant> Movies { get; set; }
} }
public class Voice public class Voice
@ -59,11 +58,4 @@ namespace Makhno.Models
public string Poster { get; set; } public string Poster { get; set; }
public string Subtitle { get; set; } public string Subtitle { get; set; }
} }
public class MovieVariant
{
public string Title { get; set; }
public string File { get; set; }
public string Quality { get; set; }
}
} }

View File

@ -169,21 +169,6 @@ namespace Mikai.Controllers
if (NeedsResolve(voice.ProviderName, episode.Url)) if (NeedsResolve(voice.ProviderName, episode.Url))
{ {
if (episode.Url.Contains("ashdi.vip/vod", StringComparison.OrdinalIgnoreCase))
{
var ashdiStreams = await invoke.ParseAshdiPageStreams(episode.Url);
if (ashdiStreams != null && ashdiStreams.Count > 0)
{
foreach (var ashdiStream in ashdiStreams)
{
string optionName = $"{voice.DisplayName} {ashdiStream.title}";
string ashdiCallUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
movieTpl.Append(optionName, accsArgs(ashdiCallUrl), "call");
}
continue;
}
}
string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}"; string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call"); movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call");
} }

View File

@ -4,8 +4,6 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using System.Net;
using System.Text.RegularExpressions;
using Mikai.Models; using Mikai.Models;
using Shared; using Shared;
using Shared.Engine; using Shared.Engine;
@ -16,9 +14,6 @@ namespace Mikai
{ {
public class MikaiInvoke public class MikaiInvoke
{ {
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
private readonly OnlinesSettings _init; private readonly OnlinesSettings _init;
private readonly IHybridCache _hybridCache; private readonly IHybridCache _hybridCache;
private readonly Action<string> _onLog; private readonly Action<string> _onLog;
@ -173,13 +168,6 @@ namespace Mikai
public async Task<string> ParseAshdiPage(string url) public async Task<string> ParseAshdiPage(string url)
{ {
var streams = await ParseAshdiPageStreams(url);
return streams?.FirstOrDefault().link;
}
public async Task<List<(string title, string link)>> ParseAshdiPageStreams(string url)
{
var streams = new List<(string title, string link)>();
try try
{ {
var headers = new List<HeadersModel>() var headers = new List<HeadersModel>()
@ -188,56 +176,22 @@ namespace Mikai
new HeadersModel("Referer", "https://ashdi.vip/") new HeadersModel("Referer", "https://ashdi.vip/")
}; };
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url)); string requestUrl = AshdiRequestUrl(url);
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}"); _onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get()); string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html)) if (string.IsNullOrEmpty(html))
return streams; return null;
string rawArray = ExtractPlayerFileArray(html); var match = System.Text.RegularExpressions.Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
if (!string.IsNullOrWhiteSpace(rawArray))
{
string json = WebUtility.HtmlDecode(rawArray)
.Replace("\\/", "/")
.Replace("\\'", "'")
.Replace("\\\"", "\"");
using var jsonDoc = JsonDocument.Parse(json);
if (jsonDoc.RootElement.ValueKind == JsonValueKind.Array)
{
int index = 1;
foreach (var item in jsonDoc.RootElement.EnumerateArray())
{
if (!item.TryGetProperty("file", out var fileProp))
continue;
string file = fileProp.GetString();
if (string.IsNullOrWhiteSpace(file))
continue;
string rawTitle = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null;
streams.Add((BuildDisplayTitle(rawTitle, file, index), file));
index++;
}
if (streams.Count > 0)
return streams;
}
}
var match = Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
if (match.Success) if (match.Success)
{ return match.Groups[1].Value;
string file = match.Groups[1].Value;
streams.Add((BuildDisplayTitle("Основне джерело", file, 1), file));
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_onLog($"Mikai ParseAshdiPage error: {ex.Message}"); _onLog($"Mikai ParseAshdiPage error: {ex.Message}");
} }
return streams; return null;
} }
private List<HeadersModel> DefaultHeaders() private List<HeadersModel> DefaultHeaders()
@ -250,168 +204,6 @@ namespace Mikai
}; };
} }
private static string WithAshdiMultivoice(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
return url;
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
return url;
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
}
private static string BuildDisplayTitle(string rawTitle, string link, int index)
{
string normalized = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
string qualityTag = DetectQualityTag($"{normalized} {link}");
if (string.IsNullOrWhiteSpace(qualityTag))
return normalized;
if (normalized.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || normalized.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
return normalized;
return $"{qualityTag} {normalized}";
}
private static string DetectQualityTag(string value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
if (Quality4kRegex.IsMatch(value))
return "[4K]";
if (QualityFhdRegex.IsMatch(value))
return "[FHD]";
return null;
}
private static string StripMoviePrefix(string title)
{
if (string.IsNullOrWhiteSpace(title))
return title;
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
return normalized;
string prefix = normalized.Substring(0, sepIndex).Trim();
string suffix = normalized.Substring(sepIndex + 3).Trim();
if (string.IsNullOrWhiteSpace(suffix))
return normalized;
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
return suffix;
return normalized;
}
private static string ExtractPlayerFileArray(string html)
{
if (string.IsNullOrWhiteSpace(html))
return null;
int searchIndex = 0;
while (searchIndex >= 0 && searchIndex < html.Length)
{
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
if (fileIndex < 0)
return null;
int colonIndex = html.IndexOf(':', fileIndex);
if (colonIndex < 0)
return null;
int startIndex = colonIndex + 1;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
{
startIndex++;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
}
if (startIndex >= html.Length || html[startIndex] != '[')
{
searchIndex = fileIndex + 4;
continue;
}
return ExtractBracketArray(html, startIndex);
}
return null;
}
private static string ExtractBracketArray(string text, int startIndex)
{
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
return null;
int depth = 0;
bool inString = false;
bool escaped = false;
char quoteChar = '\0';
for (int i = startIndex; i < text.Length; i++)
{
char ch = text[i];
if (inString)
{
if (escaped)
{
escaped = false;
continue;
}
if (ch == '\\')
{
escaped = true;
continue;
}
if (ch == quoteChar)
{
inString = false;
quoteChar = '\0';
}
continue;
}
if (ch == '"' || ch == '\'')
{
inString = true;
quoteChar = ch;
continue;
}
if (ch == '[')
{
depth++;
continue;
}
if (ch == ']')
{
depth--;
if (depth == 0)
return text.Substring(startIndex, i - startIndex + 1);
}
}
return null;
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1) 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) if (init != null && init.rhub && rhub != -1)

View File

@ -24,7 +24,7 @@ namespace Mikai
{ {
public class ModInit public class ModInit
{ {
public static double Version => 3.7; public static double Version => 3.6;
public static OnlinesSettings Mikai; public static OnlinesSettings Mikai;
public static bool ApnHostProvided; public static bool ApnHostProvided;

View File

@ -281,31 +281,12 @@ namespace UaTUT
continue; continue;
var playerData = await invoke.GetPlayerData(playerUrl); var playerData = await invoke.GetPlayerData(playerUrl);
var movieStreams = playerData?.Movies? if (playerData?.File == null)
.Where(m => m != null && !string.IsNullOrEmpty(m.File))
.ToList() ?? new List<MovieVariant>();
if (movieStreams.Count == 0 && !string.IsNullOrEmpty(playerData?.File))
{
movieStreams.Add(new MovieVariant
{
File = playerData.File,
Title = "Основне джерело",
Quality = "auto"
});
}
if (movieStreams.Count == 0)
continue; continue;
foreach (var variant in movieStreams) string movieName = $"{movie.Title} ({movie.Year})";
{ string movieLink = $"{host}/uatut/play/movie?imdb_id={movie.Id}&title={HttpUtility.UrlEncode(movie.Title)}&year={movie.Year}";
string label = !string.IsNullOrWhiteSpace(variant.Title) movie_tpl.Append(movieName, movieLink, "call");
? variant.Title
: "Варіант";
movie_tpl.Append(label, BuildStreamUrl(init, variant.File));
}
} }
if (movie_tpl.data == null || movie_tpl.data.Count == 0) if (movie_tpl.data == null || movie_tpl.data.Count == 0)
@ -320,7 +301,7 @@ namespace UaTUT
[HttpGet] [HttpGet]
[Route("play/movie")] [Route("play/movie")]
async public Task<ActionResult> PlayMovie(long imdb_id, string title, int year, string stream = null, bool play = false, bool rjson = false) async public Task<ActionResult> PlayMovie(long imdb_id, string title, int year, bool play = false, bool rjson = false)
{ {
await UpdateService.ConnectAsync(host); await UpdateService.ConnectAsync(host);
@ -363,16 +344,12 @@ namespace UaTUT
return OnError(); return OnError();
var playerData = await invoke.GetPlayerData(playerUrl); var playerData = await invoke.GetPlayerData(playerUrl);
string selectedFile = HttpUtility.UrlDecode(stream); if (playerData?.File == null)
if (string.IsNullOrWhiteSpace(selectedFile))
selectedFile = playerData?.Movies?.FirstOrDefault(m => !string.IsNullOrWhiteSpace(m.File))?.File ?? playerData?.File;
if (string.IsNullOrWhiteSpace(selectedFile))
return OnError(); return OnError();
OnLog($"UaTUT PlayMovie: обрано потік {selectedFile}"); OnLog($"UaTUT PlayMovie: Found direct file: {playerData.File}");
string streamUrl = BuildStreamUrl(init, selectedFile); string streamUrl = BuildStreamUrl(init, playerData.File);
// Якщо play=true, робимо Redirect, інакше повертаємо JSON // Якщо play=true, робимо Redirect, інакше повертаємо JSON
if (play) if (play)

View File

@ -24,7 +24,7 @@ namespace UaTUT
{ {
public class ModInit public class ModInit
{ {
public static double Version => 3.7; public static double Version => 3.6;
public static OnlinesSettings UaTUT; public static OnlinesSettings UaTUT;
public static bool ApnHostProvided; public static bool ApnHostProvided;

View File

@ -36,7 +36,6 @@ namespace UaTUT.Models
public string Poster { get; set; } public string Poster { get; set; }
public List<Voice> Voices { get; set; } public List<Voice> Voices { get; set; }
public List<Season> Seasons { get; set; } // Залишаємо для зворотної сумісності public List<Season> Seasons { get; set; } // Залишаємо для зворотної сумісності
public List<MovieVariant> Movies { get; set; }
} }
public class Voice public class Voice
@ -59,11 +58,4 @@ namespace UaTUT.Models
public string Poster { get; set; } public string Poster { get; set; }
public string Subtitle { get; set; } public string Subtitle { get; set; }
} }
public class MovieVariant
{
public string Title { get; set; }
public string File { get; set; }
public string Quality { get; set; }
}
} }

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -17,9 +16,6 @@ namespace UaTUT
{ {
public class UaTUTInvoke public class UaTUTInvoke
{ {
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
private OnlinesSettings _init; private OnlinesSettings _init;
private IHybridCache _hybridCache; private IHybridCache _hybridCache;
private Action<string> _onLog; private Action<string> _onLog;
@ -133,18 +129,13 @@ namespace UaTUT
{ {
try try
{ {
string sourceUrl = WithAshdiMultivoice(playerUrl); string requestUrl = playerUrl;
string requestUrl = sourceUrl; if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost)) requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
requestUrl = ApnHelper.WrapUrl(_init, sourceUrl);
_onLog($"UaTUT getting player data from: {requestUrl}"); _onLog($"UaTUT getting player data from: {requestUrl}");
var headers = new List<HeadersModel>() var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
{
new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"),
new HeadersModel("Referer", sourceUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase) ? "https://ashdi.vip/" : _init.apihost)
};
var response = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get()); var response = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(response)) if (string.IsNullOrEmpty(response))
@ -170,15 +161,6 @@ namespace UaTUT
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("[")) if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
{ {
playerData.File = fileMatch.Groups[1].Value; playerData.File = fileMatch.Groups[1].Value;
playerData.Movies = new List<MovieVariant>()
{
new MovieVariant
{
File = playerData.File,
Quality = DetectQualityTag(playerData.File) ?? "auto",
Title = BuildMovieTitle("Основне джерело", playerData.File, 1)
}
};
_onLog($"UaTUT found direct file: {playerData.File}"); _onLog($"UaTUT found direct file: {playerData.File}");
// Шукаємо poster // Шукаємо poster
@ -190,19 +172,13 @@ namespace UaTUT
} }
// Для серіалів шукаємо JSON структуру з сезонами та озвучками // Для серіалів шукаємо JSON структуру з сезонами та озвучками
string jsonData = ExtractPlayerFileArray(playerHtml); var jsonMatch = Regex.Match(playerHtml, @"file:'(\[.*?\])'", RegexOptions.Singleline);
if (!string.IsNullOrWhiteSpace(jsonData)) if (jsonMatch.Success)
{ {
string normalizedJson = WebUtility.HtmlDecode(jsonData) string jsonData = jsonMatch.Groups[1].Value;
.Replace("\\/", "/")
.Replace("\\'", "'")
.Replace("\\\"", "\"");
_onLog($"UaTUT found JSON data for series"); _onLog($"UaTUT found JSON data for series");
playerData.Movies = ParseMovieVariantsJson(normalizedJson); playerData.Voices = ParseVoicesJson(jsonData);
playerData.File = playerData.Movies?.FirstOrDefault()?.File;
playerData.Voices = ParseVoicesJson(normalizedJson);
return playerData; return playerData;
} }
@ -277,202 +253,5 @@ namespace UaTUT
return new List<Voice>(); return new List<Voice>();
} }
} }
private List<MovieVariant> ParseMovieVariantsJson(string jsonData)
{
try
{
var data = JsonConvert.DeserializeObject<List<dynamic>>(jsonData);
var movies = new List<MovieVariant>();
if (data == null || data.Count == 0)
return movies;
int index = 1;
foreach (var item in data)
{
string file = item?.file?.ToString();
if (string.IsNullOrWhiteSpace(file))
continue;
string rawTitle = item?.title?.ToString();
movies.Add(new MovieVariant
{
File = file,
Quality = DetectQualityTag($"{rawTitle} {file}") ?? "auto",
Title = BuildMovieTitle(rawTitle, file, index)
});
index++;
}
return movies;
}
catch (Exception ex)
{
_onLog($"UaTUT ParseMovieVariantsJson error: {ex.Message}");
return new List<MovieVariant>();
}
}
private static string WithAshdiMultivoice(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
return url;
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
return url;
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
}
private static string BuildMovieTitle(string rawTitle, string file, int index)
{
string title = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
string qualityTag = DetectQualityTag($"{title} {file}");
if (string.IsNullOrWhiteSpace(qualityTag))
return title;
if (title.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || title.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
return title;
return $"{qualityTag} {title}";
}
private static string DetectQualityTag(string value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
if (Quality4kRegex.IsMatch(value))
return "[4K]";
if (QualityFhdRegex.IsMatch(value))
return "[FHD]";
return null;
}
private static string StripMoviePrefix(string title)
{
if (string.IsNullOrWhiteSpace(title))
return title;
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
return normalized;
string prefix = normalized.Substring(0, sepIndex).Trim();
string suffix = normalized.Substring(sepIndex + 3).Trim();
if (string.IsNullOrWhiteSpace(suffix))
return normalized;
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
return suffix;
return normalized;
}
private static string ExtractPlayerFileArray(string html)
{
if (string.IsNullOrWhiteSpace(html))
return null;
int searchIndex = 0;
while (searchIndex >= 0 && searchIndex < html.Length)
{
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
if (fileIndex < 0)
return null;
int colonIndex = html.IndexOf(':', fileIndex);
if (colonIndex < 0)
return null;
int startIndex = colonIndex + 1;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
{
startIndex++;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
}
if (startIndex >= html.Length || html[startIndex] != '[')
{
searchIndex = fileIndex + 4;
continue;
}
return ExtractBracketArray(html, startIndex);
}
return null;
}
private static string ExtractBracketArray(string text, int startIndex)
{
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
return null;
int depth = 0;
bool inString = false;
bool escaped = false;
char quoteChar = '\0';
for (int i = startIndex; i < text.Length; i++)
{
char ch = text[i];
if (inString)
{
if (escaped)
{
escaped = false;
continue;
}
if (ch == '\\')
{
escaped = true;
continue;
}
if (ch == quoteChar)
{
inString = false;
quoteChar = '\0';
}
continue;
}
if (ch == '"' || ch == '\'')
{
inString = true;
quoteChar = ch;
continue;
}
if (ch == '[')
{
depth++;
continue;
}
if (ch == ']')
{
depth--;
if (depth == 0)
return text.Substring(startIndex, i - startIndex + 1);
}
}
return null;
}
} }
} }

View File

@ -392,34 +392,9 @@ namespace Uaflix.Controllers
} }
else // Фільм else // Фільм
{ {
var playResult = await invoke.ParseEpisode(filmUrl); string link = $"{host}/uaflix?t={HttpUtility.UrlEncode(filmUrl)}&play=true";
if (playResult?.streams == null || playResult.streams.Count == 0) var tpl = new MovieTpl(title, original_title, 1);
{ tpl.Append(title, accsArgs(link), method: "play");
OnLog("=== RETURN: movie no streams ===");
return OnError("uaflix", proxyManager);
}
var tpl = new MovieTpl(title, original_title, playResult.streams.Count);
int index = 1;
foreach (var stream in playResult.streams)
{
if (stream == null || string.IsNullOrEmpty(stream.link))
continue;
string label = !string.IsNullOrWhiteSpace(stream.title)
? stream.title
: $"Варіант {index}";
tpl.Append(label, BuildStreamUrl(init, stream.link));
index++;
}
if (tpl.data == null || tpl.data.Count == 0)
{
OnLog("=== RETURN: movie template empty ===");
return OnError("uaflix", proxyManager);
}
OnLog("=== RETURN: movie template ==="); OnLog("=== RETURN: movie template ===");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
} }

View File

@ -25,7 +25,7 @@ namespace Uaflix
{ {
public class ModInit public class ModInit
{ {
public static double Version => 3.8; public static double Version => 3.7;
public static OnlinesSettings UaFlix; public static OnlinesSettings UaFlix;
public static bool ApnHostProvided; public static bool ApnHostProvided;

View File

@ -6,14 +6,7 @@ namespace Uaflix.Models
public class PlayResult public class PlayResult
{ {
public string ashdi_url { get; set; } public string ashdi_url { get; set; }
public List<PlayStream> streams { get; set; } public List<(string link, string quality)> streams { get; set; }
public SubtitleTpl? subtitles { get; set; } public SubtitleTpl? subtitles { get; set; }
} }
public class PlayStream
{
public string link { get; set; }
public string quality { get; set; }
public string title { get; set; }
}
} }

View File

@ -20,9 +20,6 @@ namespace Uaflix
{ {
public class UaflixInvoke public class UaflixInvoke
{ {
private static readonly Regex Quality4kRegex = new Regex(@"(^|[^0-9])(2160p?)([^0-9]|$)|\b4k\b|\buhd\b", RegexOptions.IgnoreCase);
private static readonly Regex QualityFhdRegex = new Regex(@"(^|[^0-9])(1080p?)([^0-9]|$)|\bfhd\b", RegexOptions.IgnoreCase);
private OnlinesSettings _init; private OnlinesSettings _init;
private IHybridCache _hybridCache; private IHybridCache _hybridCache;
private Action<string> _onLog; private Action<string> _onLog;
@ -429,7 +426,7 @@ namespace Uaflix
/// <summary> /// <summary>
/// Парсинг одного епізоду з ashdi-vod (новий метод для обробки окремих епізодів з ashdi.vip/vod/) /// Парсинг одного епізоду з ashdi-vod (новий метод для обробки окремих епізодів з ashdi.vip/vod/)
/// </summary> /// </summary>
private async Task<List<PlayStream>> ParseAshdiVodEpisode(string iframeUrl) private async Task<(string file, string voiceName)> ParseAshdiVodEpisode(string iframeUrl)
{ {
var headers = new List<HeadersModel>() var headers = new List<HeadersModel>()
{ {
@ -437,69 +434,32 @@ namespace Uaflix
new HeadersModel("Referer", "https://uafix.net/") new HeadersModel("Referer", "https://uafix.net/")
}; };
var result = new List<PlayStream>();
try try
{ {
string requestUrl = WithAshdiMultivoice(iframeUrl); string html = await Http.Get(_init.cors(iframeUrl), headers: headers, proxy: _proxyManager.Get());
string html = await Http.Get(_init.cors(AshdiRequestUrl(requestUrl)), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return result;
string rawArray = ExtractPlayerFileArray(html); // Шукаємо Playerjs конфігурацію з file параметром
if (!string.IsNullOrWhiteSpace(rawArray))
{
string json = WebUtility.HtmlDecode(rawArray)
.Replace("\\/", "/")
.Replace("\\'", "'")
.Replace("\\\"", "\"");
var items = JsonConvert.DeserializeObject<List<JObject>>(json);
if (items != null && items.Count > 0)
{
int index = 1;
foreach (var item in items)
{
string fileUrl = item?["file"]?.ToString();
if (string.IsNullOrWhiteSpace(fileUrl))
continue;
string rawTitle = item["title"]?.ToString();
result.Add(new PlayStream
{
link = fileUrl,
quality = DetectQualityTag($"{rawTitle} {fileUrl}") ?? "auto",
title = BuildDisplayTitle(rawTitle, fileUrl, index)
});
index++;
}
if (result.Count > 0)
return result;
}
}
// Fallback для старого формату, де є лише один file
var match = Regex.Match(html, @"file:\s*'?([^'""\s,}]+\.m3u8)'?"); var match = Regex.Match(html, @"file:\s*'?([^'""\s,}]+\.m3u8)'?");
if (!match.Success) if (!match.Success)
{
// Якщо не знайдено, шукаємо в іншому форматі
match = Regex.Match(html, @"file['""]?\s*:\s*['""]([^'""}]+\.m3u8)['""]"); match = Regex.Match(html, @"file['""]?\s*:\s*['""]([^'""}]+\.m3u8)['""]");
}
if (!match.Success) if (!match.Success)
return result; return (null, null);
string fallbackFile = match.Groups[1].Value; string fileUrl = match.Groups[1].Value;
result.Add(new PlayStream
{
link = fallbackFile,
quality = DetectQualityTag(fallbackFile) ?? "auto",
title = BuildDisplayTitle(ExtractVoiceFromUrl(fallbackFile), fallbackFile, 1)
});
return result; // Визначити озвучку з URL
string voiceName = ExtractVoiceFromUrl(fileUrl);
return (fileUrl, voiceName);
} }
catch (Exception ex) catch (Exception ex)
{ {
_onLog($"ParseAshdiVodEpisode error: {ex.Message}"); _onLog($"ParseAshdiVodEpisode error: {ex.Message}");
return result; return (null, null);
} }
} }
@ -1323,7 +1283,7 @@ namespace Uaflix
public async Task<Uaflix.Models.PlayResult> ParseEpisode(string url) public async Task<Uaflix.Models.PlayResult> ParseEpisode(string url)
{ {
var result = new Uaflix.Models.PlayResult() { streams = new List<PlayStream>() }; var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() };
try try
{ {
string html = await Http.Get(_init.cors(url), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get()); string html = await Http.Get(_init.cors(url), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
@ -1336,12 +1296,7 @@ namespace Uaflix
string videoUrl = videoNode.GetAttributeValue("src", ""); string videoUrl = videoNode.GetAttributeValue("src", "");
if (!string.IsNullOrEmpty(videoUrl)) if (!string.IsNullOrEmpty(videoUrl))
{ {
result.streams.Add(new PlayStream result.streams.Add((videoUrl, "1080p"));
{
link = videoUrl,
quality = "1080p",
title = BuildDisplayTitle("Основне джерело", videoUrl, 1)
});
return result; return result;
} }
} }
@ -1370,7 +1325,11 @@ namespace Uaflix
if (iframeUrl.Contains("/vod/")) if (iframeUrl.Contains("/vod/"))
{ {
// Це окремий епізод на ashdi.vip/vod/, обробляємо як ashdi-vod // Це окремий епізод на ashdi.vip/vod/, обробляємо як ashdi-vod
result.streams = await ParseAshdiVodEpisode(iframeUrl); var (file, voiceName) = await ParseAshdiVodEpisode(iframeUrl);
if (!string.IsNullOrEmpty(file))
{
result.streams.Add((file, "1080p"));
}
} }
else else
{ {
@ -1426,9 +1385,9 @@ namespace Uaflix
} }
} }
async Task<List<PlayStream>> ParseAllZetvideoSources(string iframeUrl) async Task<List<(string link, string quality)>> ParseAllZetvideoSources(string iframeUrl)
{ {
var result = new List<PlayStream>(); var result = new List<(string link, string quality)>();
var html = await Http.Get(_init.cors(iframeUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://zetvideo.net/") }, proxy: _proxyManager.Get()); var html = await Http.Get(_init.cors(iframeUrl), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://zetvideo.net/") }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html)) return result; if (string.IsNullOrEmpty(html)) return result;
@ -1441,13 +1400,7 @@ namespace Uaflix
var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)"); var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)");
if (match.Success) if (match.Success)
{ {
string link = match.Groups[1].Value; result.Add((match.Groups[1].Value, "1080p"));
result.Add(new PlayStream
{
link = link,
quality = "1080p",
title = BuildDisplayTitle("Основне джерело", link, 1)
});
return result; return result;
} }
} }
@ -1457,22 +1410,15 @@ namespace Uaflix
{ {
foreach (var node in sourceNodes) foreach (var node in sourceNodes)
{ {
string link = node.GetAttributeValue("src", null); result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p"));
string quality = node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p";
result.Add(new PlayStream
{
link = link,
quality = quality,
title = BuildDisplayTitle(quality, link, result.Count + 1)
});
} }
} }
return result; return result;
} }
async Task<List<PlayStream>> ParseAllAshdiSources(string iframeUrl) async Task<List<(string link, string quality)>> ParseAllAshdiSources(string iframeUrl)
{ {
var result = new List<PlayStream>(); var result = new List<(string link, string quality)>();
var html = await Http.Get(_init.cors(AshdiRequestUrl(iframeUrl)), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }, proxy: _proxyManager.Get()); var html = await Http.Get(_init.cors(AshdiRequestUrl(iframeUrl)), headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html)) return result; if (string.IsNullOrEmpty(html)) return result;
@ -1484,14 +1430,7 @@ namespace Uaflix
{ {
foreach (var node in sourceNodes) foreach (var node in sourceNodes)
{ {
string link = node.GetAttributeValue("src", null); result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p"));
string quality = node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p";
result.Add(new PlayStream
{
link = link,
quality = quality,
title = BuildDisplayTitle(quality, link, result.Count + 1)
});
} }
} }
return result; return result;
@ -1517,168 +1456,6 @@ namespace Uaflix
return null; return null;
} }
private static string WithAshdiMultivoice(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
if (url.IndexOf("ashdi.vip/vod/", StringComparison.OrdinalIgnoreCase) < 0)
return url;
if (url.IndexOf("multivoice", StringComparison.OrdinalIgnoreCase) >= 0)
return url;
return url.Contains("?") ? $"{url}&multivoice" : $"{url}?multivoice";
}
private static string BuildDisplayTitle(string rawTitle, string link, int index)
{
string normalized = string.IsNullOrWhiteSpace(rawTitle) ? $"Варіант {index}" : StripMoviePrefix(WebUtility.HtmlDecode(rawTitle).Trim());
string qualityTag = DetectQualityTag($"{normalized} {link}");
if (string.IsNullOrWhiteSpace(qualityTag))
return normalized;
if (normalized.StartsWith("[4K]", StringComparison.OrdinalIgnoreCase) || normalized.StartsWith("[FHD]", StringComparison.OrdinalIgnoreCase))
return normalized;
return $"{qualityTag} {normalized}";
}
private static string DetectQualityTag(string value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
if (Quality4kRegex.IsMatch(value))
return "[4K]";
if (QualityFhdRegex.IsMatch(value))
return "[FHD]";
return null;
}
private static string StripMoviePrefix(string title)
{
if (string.IsNullOrWhiteSpace(title))
return title;
string normalized = Regex.Replace(title, @"\s+", " ").Trim();
int sepIndex = normalized.LastIndexOf(" - ", StringComparison.Ordinal);
if (sepIndex <= 0 || sepIndex >= normalized.Length - 3)
return normalized;
string prefix = normalized.Substring(0, sepIndex).Trim();
string suffix = normalized.Substring(sepIndex + 3).Trim();
if (string.IsNullOrWhiteSpace(suffix))
return normalized;
if (Regex.IsMatch(prefix, @"(19|20)\d{2}"))
return suffix;
return normalized;
}
private static string ExtractPlayerFileArray(string html)
{
if (string.IsNullOrWhiteSpace(html))
return null;
int searchIndex = 0;
while (searchIndex >= 0 && searchIndex < html.Length)
{
int fileIndex = html.IndexOf("file", searchIndex, StringComparison.OrdinalIgnoreCase);
if (fileIndex < 0)
return null;
int colonIndex = html.IndexOf(':', fileIndex);
if (colonIndex < 0)
return null;
int startIndex = colonIndex + 1;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
if (startIndex < html.Length && (html[startIndex] == '\'' || html[startIndex] == '"'))
{
startIndex++;
while (startIndex < html.Length && char.IsWhiteSpace(html[startIndex]))
startIndex++;
}
if (startIndex >= html.Length || html[startIndex] != '[')
{
searchIndex = fileIndex + 4;
continue;
}
return ExtractBracketArray(html, startIndex);
}
return null;
}
private static string ExtractBracketArray(string text, int startIndex)
{
if (startIndex < 0 || startIndex >= text.Length || text[startIndex] != '[')
return null;
int depth = 0;
bool inString = false;
bool escaped = false;
char quoteChar = '\0';
for (int i = startIndex; i < text.Length; i++)
{
char ch = text[i];
if (inString)
{
if (escaped)
{
escaped = false;
continue;
}
if (ch == '\\')
{
escaped = true;
continue;
}
if (ch == quoteChar)
{
inString = false;
quoteChar = '\0';
}
continue;
}
if (ch == '"' || ch == '\'')
{
inString = true;
quoteChar = ch;
continue;
}
if (ch == '[')
{
depth++;
continue;
}
if (ch == ']')
{
depth--;
if (depth == 0)
return text.Substring(startIndex, i - startIndex + 1);
}
}
return null;
}
sealed class EpisodePlayerInfo sealed class EpisodePlayerInfo
{ {
public string IframeUrl { get; set; } public string IframeUrl { get; set; }