mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 09:22:21 +00:00
perf(uaflix): implement lazy season parsing for serials
Refactor season selection logic to use lazy loading instead of full aggregation, improving performance when choosing seasons. Added GetSeasonIndex and GetSeasonEpisodes methods, and SeasonUrls property to PaginationInfo for efficient season URL management.
This commit is contained in:
parent
31549455ee
commit
0aed459fab
@ -163,97 +163,50 @@ namespace Uaflix.Controllers
|
||||
|
||||
if (serial == 1)
|
||||
{
|
||||
// Агрегуємо всі озвучки з усіх плеєрів
|
||||
var structure = await invoke.AggregateSerialStructure(filmUrl);
|
||||
if (structure == null || !structure.Voices.Any())
|
||||
{
|
||||
OnLog("No voices found in aggregated structure");
|
||||
OnLog("=== RETURN: no voices OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"Structure aggregated successfully: {structure.Voices.Count} voices, URL: {filmUrl}");
|
||||
foreach (var voice in structure.Voices)
|
||||
{
|
||||
OnLog($"Voice: {voice.Key}, Type: {voice.Value.PlayerType}, Seasons: {voice.Value.Seasons.Count}");
|
||||
foreach (var season in voice.Value.Seasons)
|
||||
{
|
||||
OnLog($" Season {season.Key}: {season.Value.Count} episodes");
|
||||
}
|
||||
}
|
||||
|
||||
// s == -1: Вибір сезону
|
||||
// s == -1: швидкий вибір сезону без повної агрегації серіалу
|
||||
if (s == -1)
|
||||
{
|
||||
List<int> allSeasons;
|
||||
VoiceInfo tVoice = null;
|
||||
bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out tVoice) && IsAshdiVoice(tVoice);
|
||||
if (restrictByVoice)
|
||||
{
|
||||
allSeasons = GetSeasonSet(tVoice).OrderBy(sn => sn).ToList();
|
||||
OnLog($"Ashdi voice selected (t='{t}'), seasons count={allSeasons.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
allSeasons = structure.Voices
|
||||
.SelectMany(v => GetSeasonSet(v.Value))
|
||||
.Distinct()
|
||||
.OrderBy(sn => sn)
|
||||
.ToList();
|
||||
}
|
||||
var seasonIndex = await invoke.GetSeasonIndex(filmUrl);
|
||||
var seasons = seasonIndex?.Seasons?.Keys
|
||||
.Distinct()
|
||||
.OrderBy(sn => sn)
|
||||
.ToList();
|
||||
|
||||
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
|
||||
|
||||
// Перевіряємо чи сезони містять валідні епізоди з файлами
|
||||
var seasonsWithValidEpisodes = allSeasons.Where(season =>
|
||||
structure.Voices.Values.Any(v =>
|
||||
v.Seasons.ContainsKey(season) &&
|
||||
v.Seasons[season].Any(ep => !string.IsNullOrEmpty(ep.File))
|
||||
)
|
||||
).ToList();
|
||||
|
||||
OnLog($"Seasons with valid episodes: {seasonsWithValidEpisodes.Count}");
|
||||
foreach (var season in allSeasons)
|
||||
if (seasons == null || seasons.Count == 0)
|
||||
{
|
||||
var episodesInSeason = structure.Voices.Values
|
||||
.Where(v => v.Seasons.ContainsKey(season))
|
||||
.SelectMany(v => v.Seasons[season])
|
||||
.Where(ep => !string.IsNullOrEmpty(ep.File))
|
||||
.ToList();
|
||||
OnLog($"Season {season}: {episodesInSeason.Count} valid episodes");
|
||||
}
|
||||
|
||||
if (!seasonsWithValidEpisodes.Any())
|
||||
{
|
||||
OnLog("No seasons with valid episodes found in structure");
|
||||
OnLog("=== RETURN: no valid seasons OnError ===");
|
||||
OnLog("No seasons found in season index");
|
||||
OnLog("=== RETURN: no seasons OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var season_tpl = new SeasonTpl(seasonsWithValidEpisodes.Count);
|
||||
foreach (var season in seasonsWithValidEpisodes)
|
||||
var season_tpl = new SeasonTpl(seasons.Count);
|
||||
foreach (int season in seasons)
|
||||
{
|
||||
string link = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season}&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
if (restrictByVoice)
|
||||
if (!string.IsNullOrWhiteSpace(t))
|
||||
link += $"&t={HttpUtility.UrlEncode(t)}";
|
||||
|
||||
season_tpl.Append($"{season}", link, season.ToString());
|
||||
OnLog($"Added season {season} to template");
|
||||
}
|
||||
|
||||
OnLog($"Returning season template with {seasonsWithValidEpisodes.Count} seasons");
|
||||
|
||||
var htmlContent = rjson ? season_tpl.ToJson() : season_tpl.ToHtml();
|
||||
OnLog($"Season template response length: {htmlContent.Length}");
|
||||
OnLog($"Season template HTML (first 300): {htmlContent.Substring(0, Math.Min(300, htmlContent.Length))}");
|
||||
OnLog($"=== RETURN: season template ({seasonsWithValidEpisodes.Count} seasons) ===");
|
||||
|
||||
return Content(htmlContent, rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8");
|
||||
OnLog($"=== RETURN: season template ({seasons.Count} seasons) ===");
|
||||
return Content(
|
||||
rjson ? season_tpl.ToJson() : season_tpl.ToHtml(),
|
||||
rjson ? "application/json; charset=utf-8" : "text/html; charset=utf-8"
|
||||
);
|
||||
}
|
||||
// s >= 0: Показуємо озвучки + епізоди
|
||||
else if (s >= 0)
|
||||
|
||||
// s >= 0: завантажуємо тільки потрібний сезон
|
||||
if (s >= 0)
|
||||
{
|
||||
var structure = await invoke.GetSeasonStructure(filmUrl, s);
|
||||
if (structure == null || structure.Voices == null || structure.Voices.Count == 0)
|
||||
{
|
||||
OnLog($"No voices found for season {s}");
|
||||
OnLog("=== RETURN: no voices for season OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
var voicesForSeason = structure.Voices
|
||||
.Where(v => v.Value.Seasons.ContainsKey(s))
|
||||
.Select(v => new { DisplayName = v.Key, Info = v.Value })
|
||||
.ToList();
|
||||
|
||||
@ -276,60 +229,48 @@ namespace Uaflix.Controllers
|
||||
OnLog($"Voice '{t}' not found, fallback to first voice: {t}");
|
||||
}
|
||||
|
||||
VoiceInfo selectedVoice = null;
|
||||
if (!structure.Voices.TryGetValue(t, out selectedVoice) || !selectedVoice.Seasons.ContainsKey(s) || selectedVoice.Seasons[s] == null || selectedVoice.Seasons[s].Count == 0)
|
||||
{
|
||||
var fallbackVoice = voicesForSeason.FirstOrDefault(v => v.Info.Seasons.ContainsKey(s) && v.Info.Seasons[s] != null && v.Info.Seasons[s].Count > 0);
|
||||
if (fallbackVoice == null)
|
||||
{
|
||||
OnLog($"Season {s} not found for selected voice and fallback voice missing");
|
||||
OnLog("=== RETURN: season not found for voice OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
t = fallbackVoice.DisplayName;
|
||||
selectedVoice = fallbackVoice.Info;
|
||||
OnLog($"Selected voice had no episodes, fallback to: {t}");
|
||||
}
|
||||
|
||||
// Створюємо VoiceTpl з усіма озвучками
|
||||
var voice_tpl = new VoiceTpl();
|
||||
var selectedVoiceInfo = structure.Voices[t];
|
||||
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
|
||||
bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo);
|
||||
|
||||
foreach (var voice in voicesForSeason)
|
||||
{
|
||||
bool targetIsAshdi = IsAshdiVoice(voice.Info);
|
||||
var targetSeasonSet = GetSeasonSet(voice.Info);
|
||||
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
|
||||
bool needSeasonReset = (selectedIsAshdi || targetIsAshdi) && !sameSeasonSet;
|
||||
|
||||
string voiceLink = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
if (needSeasonReset)
|
||||
voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||
else
|
||||
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}";
|
||||
|
||||
bool isActive = voice.DisplayName == t;
|
||||
voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
|
||||
}
|
||||
OnLog($"Created VoiceTpl with {voicesForSeason.Count} voices, active: {t}");
|
||||
|
||||
// Відображення епізодів для вибраної озвучки
|
||||
if (!structure.Voices.ContainsKey(t))
|
||||
{
|
||||
OnLog($"Voice '{t}' not found in structure");
|
||||
OnLog("=== RETURN: voice not found OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
if (!structure.Voices[t].Seasons.ContainsKey(s))
|
||||
{
|
||||
OnLog($"Season {s} not found for voice '{t}'");
|
||||
if (IsAshdiVoice(structure.Voices[t]))
|
||||
{
|
||||
string redirectUrl = $"{host}/lite/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s=-1&t={HttpUtility.UrlEncode(t)}&href={HttpUtility.UrlEncode(filmUrl)}";
|
||||
OnLog($"Ashdi voice missing season, redirect to season selector: {redirectUrl}");
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
OnLog("=== RETURN: season not found for voice OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
var episodes = structure.Voices[t].Seasons[s];
|
||||
var episodes = selectedVoice.Seasons[s];
|
||||
var episode_tpl = new EpisodeTpl();
|
||||
int appendedEpisodes = 0;
|
||||
|
||||
foreach (var ep in episodes)
|
||||
{
|
||||
if (ep == null || string.IsNullOrWhiteSpace(ep.File))
|
||||
continue;
|
||||
|
||||
string episodeTitle = !string.IsNullOrWhiteSpace(ep.Title) ? ep.Title : $"Епізод {ep.Number}";
|
||||
|
||||
// Для zetvideo-vod повертаємо URL епізоду з методом call
|
||||
// Для ashdi/zetvideo-serial повертаємо готове посилання з play
|
||||
var voice = structure.Voices[t];
|
||||
var voice = selectedVoice;
|
||||
|
||||
if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod")
|
||||
{
|
||||
@ -337,7 +278,7 @@ namespace Uaflix.Controllers
|
||||
// Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true
|
||||
string callUrl = $"{host}/lite/uaflix?episode_url={HttpUtility.UrlEncode(ep.File)}&imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&s={s}&e={ep.Number}";
|
||||
episode_tpl.Append(
|
||||
name: ep.Title,
|
||||
name: episodeTitle,
|
||||
title: title,
|
||||
s: s.ToString(),
|
||||
e: ep.Number.ToString(),
|
||||
@ -351,16 +292,25 @@ namespace Uaflix.Controllers
|
||||
// Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення
|
||||
string playUrl = BuildStreamUrl(init, ep.File);
|
||||
episode_tpl.Append(
|
||||
name: ep.Title,
|
||||
name: episodeTitle,
|
||||
title: title,
|
||||
s: s.ToString(),
|
||||
e: ep.Number.ToString(),
|
||||
link: playUrl
|
||||
);
|
||||
}
|
||||
|
||||
appendedEpisodes++;
|
||||
}
|
||||
|
||||
OnLog($"Created EpisodeTpl with {episodes.Count} episodes");
|
||||
if (appendedEpisodes == 0)
|
||||
{
|
||||
OnLog($"No valid episodes after filtering for season {s}, voice {t}");
|
||||
OnLog("=== RETURN: no valid episodes OnError ===");
|
||||
return OnError("uaflix", refresh_proxy: true);
|
||||
}
|
||||
|
||||
OnLog($"Created EpisodeTpl with {appendedEpisodes} episodes");
|
||||
|
||||
// Повертаємо VoiceTpl + EpisodeTpl разом
|
||||
episode_tpl.Append(voice_tpl);
|
||||
@ -470,25 +420,6 @@ namespace Uaflix.Controllers
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static bool IsAshdiVoice(VoiceInfo voice)
|
||||
{
|
||||
if (voice == null || string.IsNullOrEmpty(voice.PlayerType))
|
||||
return false;
|
||||
|
||||
return voice.PlayerType == "ashdi-serial" || voice.PlayerType == "ashdi-vod";
|
||||
}
|
||||
|
||||
private static HashSet<int> GetSeasonSet(VoiceInfo voice)
|
||||
{
|
||||
if (voice?.Seasons == null || voice.Seasons.Count == 0)
|
||||
return new HashSet<int>();
|
||||
|
||||
return voice.Seasons
|
||||
.Where(kv => kv.Value != null && kv.Value.Any(ep => !string.IsNullOrEmpty(ep.File)))
|
||||
.Select(kv => kv.Key)
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
private static bool IsCheckOnlineSearchEnabled()
|
||||
{
|
||||
try
|
||||
|
||||
@ -19,7 +19,7 @@ namespace Uaflix
|
||||
{
|
||||
public class ModInit : IModuleLoaded
|
||||
{
|
||||
public static double Version => 5.0;
|
||||
public static double Version => 5.1;
|
||||
|
||||
public static UaflixSettings UaFlix;
|
||||
|
||||
|
||||
@ -7,6 +7,9 @@ namespace Uaflix.Models
|
||||
{
|
||||
// Словник сезонів, де ключ - номер сезону, значення - кількість сторінок
|
||||
public Dictionary<int, int> Seasons { get; set; } = new Dictionary<int, int>();
|
||||
|
||||
// URL сторінки сезону: ключ - номер сезону, значення - абсолютний URL сторінки
|
||||
public Dictionary<int, string> SeasonUrls { get; set; } = new Dictionary<int, string>();
|
||||
|
||||
// Загальна кількість сторінок (якщо потрібно)
|
||||
public int TotalPages { get; set; }
|
||||
@ -16,4 +19,4 @@ namespace Uaflix.Models
|
||||
|
||||
public List<EpisodeLinkInfo> Episodes { get; set; } = new List<EpisodeLinkInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -821,6 +821,440 @@ namespace Uaflix
|
||||
|
||||
#endregion
|
||||
|
||||
#region Сезонний (лінивий) парсинг серіалу
|
||||
|
||||
public async Task<PaginationInfo> GetSeasonIndex(string serialUrl)
|
||||
{
|
||||
string memKey = $"UaFlix:season-index:{serialUrl}";
|
||||
if (_hybridCache.TryGetValue(memKey, out PaginationInfo cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serialUrl) || !Uri.IsWellFormedUriString(serialUrl, UriKind.Absolute))
|
||||
return null;
|
||||
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||
new HeadersModel("Referer", _init.host)
|
||||
};
|
||||
|
||||
string html = await GetHtml(serialUrl, headers);
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return null;
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
var result = new PaginationInfo
|
||||
{
|
||||
SerialUrl = serialUrl
|
||||
};
|
||||
|
||||
var seasonNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'sez-wr')]//a");
|
||||
if (seasonNodes == null)
|
||||
seasonNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'fss-box')]//a");
|
||||
|
||||
if (seasonNodes == null || seasonNodes.Count == 0)
|
||||
{
|
||||
// Якщо явного списку сезонів немає, вважаємо що є один сезон.
|
||||
result.Seasons[1] = 1;
|
||||
result.SeasonUrls[1] = serialUrl;
|
||||
_hybridCache.Set(memKey, result, cacheTime(40));
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var node in seasonNodes)
|
||||
{
|
||||
string href = node.GetAttributeValue("href", null);
|
||||
string seasonUrl = ToAbsoluteUrl(href);
|
||||
if (string.IsNullOrWhiteSpace(seasonUrl))
|
||||
continue;
|
||||
|
||||
string tabText = WebUtility.HtmlDecode(node.InnerText ?? string.Empty);
|
||||
if (!IsSeasonTabLink(seasonUrl, tabText))
|
||||
continue;
|
||||
|
||||
int season = ExtractSeasonNumber(seasonUrl, tabText);
|
||||
if (season <= 0)
|
||||
continue;
|
||||
|
||||
if (!result.SeasonUrls.TryGetValue(season, out string existing))
|
||||
{
|
||||
result.SeasonUrls[season] = seasonUrl;
|
||||
result.Seasons[season] = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsPreferableSeasonUrl(existing, seasonUrl, season))
|
||||
result.SeasonUrls[season] = seasonUrl;
|
||||
}
|
||||
|
||||
if (result.SeasonUrls.Count == 0)
|
||||
{
|
||||
result.Seasons[1] = 1;
|
||||
result.SeasonUrls[1] = serialUrl;
|
||||
}
|
||||
|
||||
_hybridCache.Set(memKey, result, cacheTime(40));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog($"GetSeasonIndex error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<EpisodeLinkInfo>> GetSeasonEpisodes(string serialUrl, int season)
|
||||
{
|
||||
if (season < 0)
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
string memKey = $"UaFlix:season-episodes:{serialUrl}:{season}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<EpisodeLinkInfo> cached))
|
||||
return cached;
|
||||
|
||||
try
|
||||
{
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||
new HeadersModel("Referer", _init.host)
|
||||
};
|
||||
|
||||
var index = await GetSeasonIndex(serialUrl);
|
||||
string seasonUrl = index?.SeasonUrls != null && index.SeasonUrls.TryGetValue(season, out string mapped)
|
||||
? mapped
|
||||
: serialUrl;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(seasonUrl))
|
||||
seasonUrl = serialUrl;
|
||||
|
||||
string html = await GetHtml(seasonUrl, headers);
|
||||
if (string.IsNullOrWhiteSpace(html) && !string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
|
||||
html = await GetHtml(serialUrl, headers);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
var result = ParseSeasonEpisodesFromHtml(html, season);
|
||||
if (result.Count == 0 && !string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string serialHtml = await GetHtml(serialUrl, headers);
|
||||
if (!string.IsNullOrWhiteSpace(serialHtml))
|
||||
result = ParseSeasonEpisodesFromHtml(serialHtml, season);
|
||||
}
|
||||
|
||||
if (result.Count == 0 && season == 1 && string.Equals(seasonUrl, serialUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Fallback для сторінок без окремих епізодів.
|
||||
result.Add(new EpisodeLinkInfo
|
||||
{
|
||||
url = serialUrl,
|
||||
title = "Епізод 1",
|
||||
season = 1,
|
||||
episode = 1
|
||||
});
|
||||
}
|
||||
|
||||
_hybridCache.Set(memKey, result, cacheTime(20));
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog($"GetSeasonEpisodes error: {ex.Message}");
|
||||
return new List<EpisodeLinkInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
List<EpisodeLinkInfo> ParseSeasonEpisodesFromHtml(string html, int season)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels')]//a[contains(@class, 'vi-img')]");
|
||||
if (episodeNodes == null || episodeNodes.Count == 0)
|
||||
return new List<EpisodeLinkInfo>();
|
||||
|
||||
var episodes = new List<EpisodeLinkInfo>();
|
||||
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
int fallbackEpisode = 1;
|
||||
|
||||
foreach (var episodeNode in episodeNodes)
|
||||
{
|
||||
string episodeUrl = ToAbsoluteUrl(episodeNode.GetAttributeValue("href", null));
|
||||
if (string.IsNullOrWhiteSpace(episodeUrl) || !used.Add(episodeUrl))
|
||||
continue;
|
||||
|
||||
var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)", RegexOptions.IgnoreCase);
|
||||
int parsedSeason = season;
|
||||
int parsedEpisode = fallbackEpisode;
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
if (int.TryParse(match.Groups[1].Value, out int seasonFromUrl))
|
||||
parsedSeason = seasonFromUrl;
|
||||
if (int.TryParse(match.Groups[2].Value, out int episodeFromUrl))
|
||||
parsedEpisode = episodeFromUrl;
|
||||
}
|
||||
|
||||
episodes.Add(new EpisodeLinkInfo
|
||||
{
|
||||
url = episodeUrl,
|
||||
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {parsedEpisode}",
|
||||
season = parsedSeason,
|
||||
episode = parsedEpisode
|
||||
});
|
||||
|
||||
fallbackEpisode = Math.Max(fallbackEpisode, parsedEpisode + 1);
|
||||
}
|
||||
|
||||
return episodes
|
||||
.Where(e => e != null && !string.IsNullOrWhiteSpace(e.url))
|
||||
.Where(e => e.season == season)
|
||||
.OrderBy(e => e.episode)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<SerialAggregatedStructure> GetSeasonStructure(string serialUrl, int season)
|
||||
{
|
||||
if (season < 0)
|
||||
return null;
|
||||
|
||||
string memKey = $"UaFlix:season-structure:{serialUrl}:{season}";
|
||||
if (_hybridCache.TryGetValue(memKey, out SerialAggregatedStructure cached))
|
||||
{
|
||||
_onLog($"GetSeasonStructure: Using cached structure for season={season}, url={serialUrl}");
|
||||
return cached;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var seasonEpisodes = await GetSeasonEpisodes(serialUrl, season);
|
||||
if (seasonEpisodes == null || seasonEpisodes.Count == 0)
|
||||
{
|
||||
_onLog($"GetSeasonStructure: No episodes for season={season}, url={serialUrl}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var structure = new SerialAggregatedStructure
|
||||
{
|
||||
SerialUrl = serialUrl,
|
||||
AllEpisodes = seasonEpisodes
|
||||
};
|
||||
|
||||
var seasonProbe = await ProbeSeasonPlayer(seasonEpisodes);
|
||||
if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
|
||||
{
|
||||
// fallback: інколи плеєр є лише на головній сторінці
|
||||
seasonProbe = await ProbeEpisodePlayer(serialUrl);
|
||||
if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
|
||||
{
|
||||
_onLog($"GetSeasonStructure: unsupported player for season={season}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (seasonProbe.playerType == "ashdi-serial" || seasonProbe.playerType == "zetvideo-serial")
|
||||
{
|
||||
var voices = await ParseMultiEpisodePlayerCached(seasonProbe.iframeUrl, seasonProbe.playerType);
|
||||
foreach (var voice in voices)
|
||||
{
|
||||
if (voice?.Seasons == null || !voice.Seasons.TryGetValue(season, out List<EpisodeInfo> seasonVoiceEpisodes) || seasonVoiceEpisodes == null || seasonVoiceEpisodes.Count == 0)
|
||||
continue;
|
||||
|
||||
structure.Voices[voice.DisplayName] = new VoiceInfo
|
||||
{
|
||||
Name = voice.Name,
|
||||
PlayerType = voice.PlayerType,
|
||||
DisplayName = voice.DisplayName,
|
||||
Seasons = new Dictionary<int, List<EpisodeInfo>>
|
||||
{
|
||||
[season] = seasonVoiceEpisodes
|
||||
.Where(ep => ep != null && !string.IsNullOrWhiteSpace(ep.File))
|
||||
.Select(ep => new EpisodeInfo
|
||||
{
|
||||
Number = ep.Number,
|
||||
Title = ep.Title,
|
||||
File = ep.File,
|
||||
Id = ep.Id,
|
||||
Poster = ep.Poster,
|
||||
Subtitle = ep.Subtitle
|
||||
})
|
||||
.ToList()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (seasonProbe.playerType == "ashdi-vod" || seasonProbe.playerType == "zetvideo-vod")
|
||||
{
|
||||
AddVodSeasonEpisodes(structure, seasonProbe.playerType, season, seasonEpisodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
_onLog($"GetSeasonStructure: player '{seasonProbe.playerType}' is not supported");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!structure.Voices.Any())
|
||||
{
|
||||
_onLog($"GetSeasonStructure: voices are empty for season={season}, url={serialUrl}");
|
||||
return null;
|
||||
}
|
||||
|
||||
NormalizeUaflixVoiceNames(structure);
|
||||
_hybridCache.Set(memKey, structure, cacheTime(30));
|
||||
return structure;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog($"GetSeasonStructure error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<List<VoiceInfo>> ParseMultiEpisodePlayerCached(string iframeUrl, string playerType)
|
||||
{
|
||||
string serialKey = NormalizeSerialPlayerKey(playerType, iframeUrl);
|
||||
string memKey = $"UaFlix:player-voices:{playerType}:{serialKey}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<VoiceInfo> cached))
|
||||
return CloneVoices(cached);
|
||||
|
||||
var parsed = await ParseMultiEpisodePlayer(iframeUrl, playerType);
|
||||
if (parsed == null || parsed.Count == 0)
|
||||
return new List<VoiceInfo>();
|
||||
|
||||
_hybridCache.Set(memKey, parsed, cacheTime(40));
|
||||
return CloneVoices(parsed);
|
||||
}
|
||||
|
||||
static List<VoiceInfo> CloneVoices(List<VoiceInfo> voices)
|
||||
{
|
||||
if (voices == null || voices.Count == 0)
|
||||
return new List<VoiceInfo>();
|
||||
|
||||
var result = new List<VoiceInfo>(voices.Count);
|
||||
foreach (var voice in voices)
|
||||
{
|
||||
if (voice == null)
|
||||
continue;
|
||||
|
||||
var clone = new VoiceInfo
|
||||
{
|
||||
Name = voice.Name,
|
||||
PlayerType = voice.PlayerType,
|
||||
DisplayName = voice.DisplayName,
|
||||
Seasons = new Dictionary<int, List<EpisodeInfo>>()
|
||||
};
|
||||
|
||||
if (voice.Seasons != null)
|
||||
{
|
||||
foreach (var season in voice.Seasons)
|
||||
{
|
||||
clone.Seasons[season.Key] = season.Value?
|
||||
.Where(ep => ep != null)
|
||||
.Select(ep => new EpisodeInfo
|
||||
{
|
||||
Number = ep.Number,
|
||||
Title = ep.Title,
|
||||
File = ep.File,
|
||||
Id = ep.Id,
|
||||
Poster = ep.Poster,
|
||||
Subtitle = ep.Subtitle
|
||||
})
|
||||
.ToList() ?? new List<EpisodeInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(clone);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
string ToAbsoluteUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return null;
|
||||
|
||||
string clean = WebUtility.HtmlDecode(url.Trim());
|
||||
if (clean.StartsWith("//"))
|
||||
clean = "https:" + clean;
|
||||
|
||||
if (clean.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || clean.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
return clean;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_init?.host))
|
||||
return clean;
|
||||
|
||||
return $"{_init.host.TrimEnd('/')}/{clean.TrimStart('/')}";
|
||||
}
|
||||
|
||||
static bool IsSeasonTabLink(string url, string text)
|
||||
{
|
||||
string u = (url ?? string.Empty).ToLowerInvariant();
|
||||
string t = (text ?? string.Empty).ToLowerInvariant();
|
||||
|
||||
if (u.Contains("/date/") || t.Contains("графік") || t.Contains("дата виходу"))
|
||||
return false;
|
||||
|
||||
if (Regex.IsMatch(u, @"(?:sezon|season)[-_/ ]?\d+", RegexOptions.IgnoreCase))
|
||||
return true;
|
||||
|
||||
if (Regex.IsMatch(t, @"(?:сезон|season)\s*\d+", RegexOptions.IgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsPreferableSeasonUrl(string oldUrl, string newUrl, int season)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newUrl))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(oldUrl))
|
||||
return true;
|
||||
|
||||
string marker = $"/sezon-{season}/";
|
||||
bool oldHasMarker = oldUrl.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool newHasMarker = newUrl.IndexOf(marker, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
if (!oldHasMarker && newHasMarker)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static int ExtractSeasonNumber(string url, string text)
|
||||
{
|
||||
foreach (string source in new[] { url, text })
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
continue;
|
||||
|
||||
var seasonBySlug = Regex.Match(source, @"(?:sezon|season)[-_/ ]?(\d+)", RegexOptions.IgnoreCase);
|
||||
if (seasonBySlug.Success && int.TryParse(seasonBySlug.Groups[1].Value, out int seasonSlug) && seasonSlug > 0)
|
||||
return seasonSlug;
|
||||
|
||||
var seasonByWordUa = Regex.Match(source, @"сезон\s*(\d+)", RegexOptions.IgnoreCase);
|
||||
if (seasonByWordUa.Success && int.TryParse(seasonByWordUa.Groups[1].Value, out int seasonWordUa) && seasonWordUa > 0)
|
||||
return seasonWordUa;
|
||||
|
||||
var seasonByWordEn = Regex.Match(source, @"season\s*(\d+)", RegexOptions.IgnoreCase);
|
||||
if (seasonByWordEn.Success && int.TryParse(seasonByWordEn.Groups[1].Value, out int seasonWordEn) && seasonWordEn > 0)
|
||||
return seasonWordEn;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<List<SearchResult>> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, int serial, string original_language, string source, string search_query)
|
||||
{
|
||||
bool allowAnime = IsAnimeRequest(title, original_title, original_language, source);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user