mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 09:22:21 +00:00
Add multivoice and quality for Movie
This commit is contained in:
parent
104afc463e
commit
e8f10e1e18
@ -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,147 @@ 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}" : 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 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)
|
||||
|
||||
@ -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)}";
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,50 @@ 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 = 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 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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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,48 @@ 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}" : 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;
|
||||
}
|
||||
|
||||
public string ExtractAshdiPath(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(ashdiStream.link)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
movieTpl.Append(optionName, accsArgs(callUrl), "call");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
string callUrl = $"{host}/mikai/play?url={HttpUtility.UrlEncode(episode.Url)}&title={HttpUtility.UrlEncode(displayTitle)}";
|
||||
movieTpl.Append(voice.DisplayName, accsArgs(callUrl), "call");
|
||||
}
|
||||
|
||||
@ -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,147 @@ 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}" : 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 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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -281,12 +281,33 @@ 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 variantName = !string.IsNullOrWhiteSpace(variant.Title)
|
||||
? variant.Title
|
||||
: "Варіант";
|
||||
|
||||
string label = $"{movieName} - {variantName}";
|
||||
movie_tpl.Append(label, BuildStreamUrl(init, variant.File));
|
||||
}
|
||||
}
|
||||
|
||||
if (movie_tpl.data == null || movie_tpl.data.Count == 0)
|
||||
@ -301,7 +322,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 +365,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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,181 @@ 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}" : 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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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,147 @@ 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}" : 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 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; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user