Compare commits

...

4 Commits

Author SHA1 Message Date
Felix
4d4ac22601
Merge pull request #18 from lampame/ashdi-quality
Ashdi quality
2026-02-21 13:42:22 +02:00
Felix
cd6724b452 refactor: strip year prefix from movie titles
Extract StripMoviePrefix method across all movie providers to remove year
prefixes (e.g., "2023 - Movie Title" → "Movie Title") from displayed titles.
Simplify label construction in UaTUT controller by removing redundant movie
name prefix from variant labels.
2026-02-21 13:37:58 +02:00
Felix
224f11b1f0 refactor: rename callUrl to ashdiCallUrl for improved clarity. 2026-02-21 13:22:47 +02:00
Felix
e8f10e1e18 Add multivoice and quality for Movie 2026-02-21 13:17:08 +02:00
20 changed files with 1265 additions and 88 deletions

View File

@ -7,6 +7,8 @@ using Shared.Models;
using System.Text.Json;
using System.Linq;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using AnimeON.Models;
using Shared.Engine;
@ -14,6 +16,9 @@ namespace AnimeON
{
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 IHybridCache _hybridCache;
private Action<string> _onLog;
@ -184,6 +189,13 @@ namespace AnimeON
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
{
var headers = new List<HeadersModel>()
@ -192,16 +204,48 @@ namespace AnimeON
new HeadersModel("Referer", "https://ashdi.vip/")
};
string requestUrl = AshdiRequestUrl(url);
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url));
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
return streams;
var match = System.Text.RegularExpressions.Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
string rawArray = ExtractPlayerFileArray(html);
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)
{
return match.Groups[1].Value;
string file = match.Groups[1].Value;
streams.Add((BuildDisplayTitle("Основне джерело", file, 1), file));
}
}
catch (Exception ex)
@ -209,7 +253,7 @@ namespace AnimeON
_onLog($"AnimeON ParseAshdiPage error: {ex.Message}");
}
return null;
return streams;
}
public async Task<string> ResolveEpisodeStream(int episodeId)
@ -260,6 +304,168 @@ namespace AnimeON
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)
{
if (init != null && init.rhub && rhub != -1)

View File

@ -223,6 +223,21 @@ namespace AnimeON.Controllers
string translationName = $"[{player.Name}] {fundub.Fundub.Name}";
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"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";

View File

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

View File

@ -21,6 +21,8 @@ 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 YearRegex = new Regex(@"(19|20)\d{2}", 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 IHybridCache _hybridCache;
@ -181,7 +183,7 @@ namespace KlonFUN
try
{
string playerHtml = await GetPlayerHtml(playerUrl);
string playerHtml = await GetPlayerHtml(WithAshdiMultivoice(playerUrl));
if (string.IsNullOrWhiteSpace(playerHtml))
return null;
@ -197,9 +199,7 @@ namespace KlonFUN
if (string.IsNullOrWhiteSpace(link))
continue;
string voiceTitle = CleanText(item.Value<string>("title"));
if (string.IsNullOrWhiteSpace(voiceTitle))
voiceTitle = $"Варіант {index}";
string voiceTitle = FormatMovieTitle(item.Value<string>("title"), link, index);
streams.Add(new MovieStream
{
@ -218,7 +218,7 @@ namespace KlonFUN
{
streams.Add(new MovieStream
{
Title = "Основне джерело",
Title = FormatMovieTitle("Основне джерело", directMatch.Groups["url"].Value, 1),
Link = directMatch.Groups["url"].Value
});
}
@ -634,6 +634,71 @@ namespace KlonFUN
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)
{
if (init != null && init.rhub && rhub != -1)

View File

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

View File

@ -173,15 +173,37 @@ namespace Makhno
return await invoke.GetPlayerData(playUrl);
});
if (playerData?.File == null)
var movieStreams = playerData?.Movies?
.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");
return OnError();
}
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";
var tpl = new MovieTpl(title ?? original_title, original_title, 1);
tpl.Append(title ?? original_title, accsArgs(movieLink), method: "play");
var tpl = new MovieTpl(title ?? original_title, original_title, movieStreams.Count);
int index = 1;
foreach (var stream in movieStreams)
{
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");
}

View File

@ -19,6 +19,8 @@ namespace Makhno
{
private const string WormholeHost = "http://wormhole.lampame.v6.rocks/";
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 IHybridCache _hybridCache;
@ -201,19 +203,20 @@ namespace Makhno
try
{
string requestUrl = playerUrl;
string sourceUrl = WithAshdiMultivoice(playerUrl);
string requestUrl = sourceUrl;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", Http.UserAgent)
};
if (playerUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
if (sourceUrl.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
{
headers.Add(new HeadersModel("Referer", "https://ashdi.vip/"));
}
if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
requestUrl = ApnHelper.WrapUrl(_init, sourceUrl);
_onLog($"Makhno getting player data from: {requestUrl}");
@ -243,12 +246,22 @@ namespace Makhno
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
{
string file = fileMatch.Groups[1].Value;
var posterMatch = Regex.Match(html, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
return new PlayerData
{
File = fileMatch.Groups[1].Value,
File = file,
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)
}
}
};
}
@ -260,12 +273,14 @@ namespace Makhno
if (!string.IsNullOrEmpty(jsonData))
{
var voices = ParseVoicesJson(jsonData);
var movies = ParseMovieVariantsJson(jsonData);
_onLog($"Makhno ParsePlayerData: voices={voices?.Count ?? 0}");
return new PlayerData
{
File = null,
File = movies.FirstOrDefault()?.File,
Poster = null,
Voices = voices
Voices = voices,
Movies = movies
};
}
@ -277,7 +292,16 @@ namespace Makhno
{
File = m3u8Match.Groups[1].Value,
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)
}
}
};
}
@ -289,7 +313,16 @@ namespace Makhno
{
File = sourceMatch.Groups[1].Value,
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)
}
}
};
}
@ -369,6 +402,41 @@ 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)
{
if (string.IsNullOrEmpty(html))
@ -531,6 +599,69 @@ namespace Makhno
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)
{
if (string.IsNullOrWhiteSpace(value))

View File

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

View File

@ -36,6 +36,7 @@ namespace Makhno.Models
public string Poster { get; set; }
public List<Voice> Voices { get; set; }
public List<Season> Seasons { get; set; }
public List<MovieVariant> Movies { get; set; }
}
public class Voice
@ -58,4 +59,11 @@ namespace Makhno.Models
public string Poster { 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,6 +169,21 @@ namespace Mikai.Controllers
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)}";
movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call");
}

View File

@ -4,6 +4,8 @@ using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using System.Net;
using System.Text.RegularExpressions;
using Mikai.Models;
using Shared;
using Shared.Engine;
@ -14,6 +16,9 @@ namespace Mikai
{
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 IHybridCache _hybridCache;
private readonly Action<string> _onLog;
@ -168,6 +173,13 @@ namespace Mikai
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
{
var headers = new List<HeadersModel>()
@ -176,22 +188,56 @@ namespace Mikai
new HeadersModel("Referer", "https://ashdi.vip/")
};
string requestUrl = AshdiRequestUrl(url);
string requestUrl = AshdiRequestUrl(WithAshdiMultivoice(url));
_onLog($"Mikai: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(_init.cors(requestUrl), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
return streams;
var match = System.Text.RegularExpressions.Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
string rawArray = ExtractPlayerFileArray(html);
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)
return match.Groups[1].Value;
{
string file = match.Groups[1].Value;
streams.Add((BuildDisplayTitle("Основне джерело", file, 1), file));
}
}
catch (Exception ex)
{
_onLog($"Mikai ParseAshdiPage error: {ex.Message}");
}
return null;
return streams;
}
private List<HeadersModel> DefaultHeaders()
@ -204,6 +250,168 @@ 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)
{
if (init != null && init.rhub && rhub != -1)

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ namespace UaTUT.Models
public string Poster { get; set; }
public List<Voice> Voices { get; set; }
public List<Season> Seasons { get; set; } // Залишаємо для зворотної сумісності
public List<MovieVariant> Movies { get; set; }
}
public class Voice
@ -58,4 +59,11 @@ namespace UaTUT.Models
public string Poster { 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,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
@ -16,6 +17,9 @@ namespace UaTUT
{
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 IHybridCache _hybridCache;
private Action<string> _onLog;
@ -129,13 +133,18 @@ namespace UaTUT
{
try
{
string requestUrl = playerUrl;
if (ApnHelper.IsAshdiUrl(playerUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
requestUrl = ApnHelper.WrapUrl(_init, playerUrl);
string sourceUrl = WithAshdiMultivoice(playerUrl);
string requestUrl = sourceUrl;
if (ApnHelper.IsAshdiUrl(sourceUrl) && ApnHelper.IsEnabled(_init) && string.IsNullOrWhiteSpace(_init.webcorshost))
requestUrl = ApnHelper.WrapUrl(_init, sourceUrl);
_onLog($"UaTUT getting player data from: {requestUrl}");
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
var headers = new List<HeadersModel>()
{
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());
if (string.IsNullOrEmpty(response))
@ -161,6 +170,15 @@ namespace UaTUT
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
{
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}");
// Шукаємо poster
@ -172,13 +190,19 @@ namespace UaTUT
}
// Для серіалів шукаємо JSON структуру з сезонами та озвучками
var jsonMatch = Regex.Match(playerHtml, @"file:'(\[.*?\])'", RegexOptions.Singleline);
if (jsonMatch.Success)
string jsonData = ExtractPlayerFileArray(playerHtml);
if (!string.IsNullOrWhiteSpace(jsonData))
{
string jsonData = jsonMatch.Groups[1].Value;
string normalizedJson = WebUtility.HtmlDecode(jsonData)
.Replace("\\/", "/")
.Replace("\\'", "'")
.Replace("\\\"", "\"");
_onLog($"UaTUT found JSON data for series");
playerData.Voices = ParseVoicesJson(jsonData);
playerData.Movies = ParseMovieVariantsJson(normalizedJson);
playerData.File = playerData.Movies?.FirstOrDefault()?.File;
playerData.Voices = ParseVoicesJson(normalizedJson);
return playerData;
}
@ -253,5 +277,202 @@ namespace UaTUT
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,9 +392,34 @@ namespace Uaflix.Controllers
}
else // Фільм
{
string link = $"{host}/uaflix?t={HttpUtility.UrlEncode(filmUrl)}&play=true";
var tpl = new MovieTpl(title, original_title, 1);
tpl.Append(title, accsArgs(link), method: "play");
var playResult = await invoke.ParseEpisode(filmUrl);
if (playResult?.streams == null || playResult.streams.Count == 0)
{
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 ===");
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 static double Version => 3.7;
public static double Version => 3.8;
public static OnlinesSettings UaFlix;
public static bool ApnHostProvided;

View File

@ -6,7 +6,14 @@ namespace Uaflix.Models
public class PlayResult
{
public string ashdi_url { get; set; }
public List<(string link, string quality)> streams { get; set; }
public List<PlayStream> streams { 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,6 +20,9 @@ namespace Uaflix
{
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 IHybridCache _hybridCache;
private Action<string> _onLog;
@ -426,7 +429,7 @@ namespace Uaflix
/// <summary>
/// Парсинг одного епізоду з ashdi-vod (новий метод для обробки окремих епізодів з ashdi.vip/vod/)
/// </summary>
private async Task<(string file, string voiceName)> ParseAshdiVodEpisode(string iframeUrl)
private async Task<List<PlayStream>> ParseAshdiVodEpisode(string iframeUrl)
{
var headers = new List<HeadersModel>()
{
@ -434,32 +437,69 @@ namespace Uaflix
new HeadersModel("Referer", "https://uafix.net/")
};
var result = new List<PlayStream>();
try
{
string html = await Http.Get(_init.cors(iframeUrl), headers: headers, proxy: _proxyManager.Get());
string requestUrl = WithAshdiMultivoice(iframeUrl);
string html = await Http.Get(_init.cors(AshdiRequestUrl(requestUrl)), headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return result;
// Шукаємо Playerjs конфігурацію з file параметром
var match = Regex.Match(html, @"file:\s*'?([^'""\s,}]+\.m3u8)'?");
if (!match.Success)
string rawArray = ExtractPlayerFileArray(html);
if (!string.IsNullOrWhiteSpace(rawArray))
{
// Якщо не знайдено, шукаємо в іншому форматі
match = Regex.Match(html, @"file['""]?\s*:\s*['""]([^'""}]+\.m3u8)['""]");
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)'?");
if (!match.Success)
return (null, null);
match = Regex.Match(html, @"file['""]?\s*:\s*['""]([^'""}]+\.m3u8)['""]");
string fileUrl = match.Groups[1].Value;
if (!match.Success)
return result;
// Визначити озвучку з URL
string voiceName = ExtractVoiceFromUrl(fileUrl);
string fallbackFile = match.Groups[1].Value;
result.Add(new PlayStream
{
link = fallbackFile,
quality = DetectQualityTag(fallbackFile) ?? "auto",
title = BuildDisplayTitle(ExtractVoiceFromUrl(fallbackFile), fallbackFile, 1)
});
return (fileUrl, voiceName);
return result;
}
catch (Exception ex)
{
_onLog($"ParseAshdiVodEpisode error: {ex.Message}");
return (null, null);
return result;
}
}
@ -1283,7 +1323,7 @@ namespace Uaflix
public async Task<Uaflix.Models.PlayResult> ParseEpisode(string url)
{
var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() };
var result = new Uaflix.Models.PlayResult() { streams = new List<PlayStream>() };
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());
@ -1296,7 +1336,12 @@ namespace Uaflix
string videoUrl = videoNode.GetAttributeValue("src", "");
if (!string.IsNullOrEmpty(videoUrl))
{
result.streams.Add((videoUrl, "1080p"));
result.streams.Add(new PlayStream
{
link = videoUrl,
quality = "1080p",
title = BuildDisplayTitle("Основне джерело", videoUrl, 1)
});
return result;
}
}
@ -1325,11 +1370,7 @@ namespace Uaflix
if (iframeUrl.Contains("/vod/"))
{
// Це окремий епізод на ashdi.vip/vod/, обробляємо як ashdi-vod
var (file, voiceName) = await ParseAshdiVodEpisode(iframeUrl);
if (!string.IsNullOrEmpty(file))
{
result.streams.Add((file, "1080p"));
}
result.streams = await ParseAshdiVodEpisode(iframeUrl);
}
else
{
@ -1385,9 +1426,9 @@ namespace Uaflix
}
}
async Task<List<(string link, string quality)>> ParseAllZetvideoSources(string iframeUrl)
async Task<List<PlayStream>> ParseAllZetvideoSources(string iframeUrl)
{
var result = new List<(string link, string quality)>();
var result = new List<PlayStream>();
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;
@ -1400,7 +1441,13 @@ namespace Uaflix
var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)");
if (match.Success)
{
result.Add((match.Groups[1].Value, "1080p"));
string link = match.Groups[1].Value;
result.Add(new PlayStream
{
link = link,
quality = "1080p",
title = BuildDisplayTitle("Основне джерело", link, 1)
});
return result;
}
}
@ -1410,15 +1457,22 @@ namespace Uaflix
{
foreach (var node in sourceNodes)
{
result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p"));
string link = node.GetAttributeValue("src", null);
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;
}
async Task<List<(string link, string quality)>> ParseAllAshdiSources(string iframeUrl)
async Task<List<PlayStream>> ParseAllAshdiSources(string iframeUrl)
{
var result = new List<(string link, string quality)>();
var result = new List<PlayStream>();
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;
@ -1430,7 +1484,14 @@ namespace Uaflix
{
foreach (var node in sourceNodes)
{
result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p"));
string link = node.GetAttributeValue("src", null);
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;
@ -1456,6 +1517,168 @@ namespace Uaflix
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
{
public string IframeUrl { get; set; }