Compare commits

..

4 Commits

Author SHA1 Message Date
Felix
0189cb929c chore(uaflix): invalidate cached season structures
Bump the season structure cache key to v3 so stale entries that may include premiere episodes are not reused.
2026-06-15 09:02:40 +03:00
Felix
b6c9c748cd fix(uaflix): return null for premiere-only seasons
Previously, seasons containing only unreleased premiere episodes were skipped during season structure iteration. This could allow downstream processing to continue without a valid season result.

Return null when every episode in a season is filtered as a premiere, making the no-available-episodes case explicit for callers.
2026-06-15 08:56:35 +03:00
Felix
ddcbc3eae8 refactor(uaflix): centralize premiere episode filtering
Filter out unreleased premiere episodes before building UAFLIX season episode structures, so downstream episode ordering, voice creation, and zetvideo handling only process available episodes. Added logging for filtered and fully-premiere seasons to make skipped content visible during probing.
2026-06-15 08:53:56 +03:00
Felix
e695121444 fix(uaflix): skip probing unreleased premiere episodes
Mark season episodes whose description title contains «Прем'єра» and skip player probing for them. This avoids unnecessary HTTP requests for episodes that have not aired yet.
2026-06-15 08:46:19 +03:00
2 changed files with 83 additions and 16 deletions

View File

@ -19,5 +19,12 @@ namespace LME.Uaflix.Models
/// Перший елемент відповідає iframeUrl, наступні — додаткові плеєри (напр. з субтитрами)
/// </summary>
public List<string> zetvideoIframeUrls { get; set; }
/// <summary>
/// Епізод позначено як «Прем'єра» (ще не вийшов) на сторінці сезону.
/// У таких епізодів у vi-desc → vi-title зазначено "Прем'єра. ДД.ММ.РРРР".
/// ProbeSeasonPlayer не робитиме зайвого HTTP-запиту для таких епізодів.
/// </summary>
public bool IsPremiere { get; set; }
}
}

View File

@ -361,6 +361,14 @@ namespace LME.Uaflix
if (episode == null || string.IsNullOrWhiteSpace(episode.url))
continue;
// Пропускаємо епізоди, позначені як «Прем'єра» — вони ще не вийшли,
// робити зайвий HTTP-запит на сторінку епізоду не потрібно.
if (episode.IsPremiere)
{
_onLog($"ProbeSeasonPlayer: Пропускаю епізод {episode.episode} — позначено як прем'єра");
continue;
}
var probed = await ProbeEpisodePlayer(episode.url);
if (probed == null)
continue;
@ -425,6 +433,17 @@ namespace LME.Uaflix
if (structure == null || string.IsNullOrWhiteSpace(playerType) || seasonEpisodes == null || seasonEpisodes.Count == 0)
return;
// Відфільтровуємо епізоди, що ще не вийшли (позначені як «Прем'єра»)
var availableEpisodes = seasonEpisodes.Where(e => !e.IsPremiere).ToList();
int filteredCount = seasonEpisodes.Count - availableEpisodes.Count;
if (filteredCount > 0)
_onLog($"AddVodSeasonEpisodes: Відфільтровано {filteredCount} прем'єрних епізодів із сезону {season}");
if (availableEpisodes.Count == 0)
{
_onLog($"AddVodSeasonEpisodes: Усі епізоди сезону {season} є прем'єрами, пропускаю");
return;
}
string displayName = playerType == "ashdi-vod" ? "Uaflix #3" : "Uaflix #2";
if (!structure.Voices.ContainsKey(displayName))
{
@ -437,7 +456,7 @@ namespace LME.Uaflix
};
}
var episodes = seasonEpisodes
var episodes = availableEpisodes
.OrderBy(ep => ep.episode)
.Select(ep => new EpisodeInfo
{
@ -1084,13 +1103,28 @@ namespace LME.Uaflix
parsedEpisode = episodeFromUrl;
}
episodes.Add(new EpisodeLinkInfo
var episode = new EpisodeLinkInfo
{
url = episodeUrl,
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {parsedEpisode}",
season = parsedSeason,
episode = parsedEpisode
});
};
// Перевірка на «Прем'єра» — епізод ще не вийшов, плеєра немає
var viDesc = episodeNode.SelectSingleNode(".//div[contains(@class, 'vi-desc')]");
if (viDesc != null)
{
var viTitle = viDesc.SelectSingleNode(".//div[contains(@class, 'vi-title')]");
string descText = viTitle?.InnerText?.Trim() ?? string.Empty;
if (descText.IndexOf("Прем'єра", StringComparison.OrdinalIgnoreCase) >= 0)
{
episode.IsPremiere = true;
_onLog($"ParseSeasonEpisodesFromHtml: Серія {parsedEpisode} позначена як прем'єра: '{descText}'");
}
}
episodes.Add(episode);
fallbackEpisode = Math.Max(fallbackEpisode, parsedEpisode + 1);
}
@ -1107,8 +1141,8 @@ namespace LME.Uaflix
if (season < 0)
return null;
// v2 — зміна кеш-ключа для інвалідації старих структур без multi-voice
string memKey = $"lme_uaflix:season-structure-v2:{serialUrl}:{season}";
// v3 — інвалідація старих структур, що містили прем'єрні серії
string memKey = $"lme_uaflix:season-structure-v3:{serialUrl}:{season}";
if (_hybridCache.TryGetValue(memKey, out SerialAggregatedStructure cached))
{
_onLog($"GetSeasonStructure: Using cached structure for season={season}, url={serialUrl}");
@ -1175,13 +1209,24 @@ namespace LME.Uaflix
}
else if (seasonProbe.PlayerType == "ashdi-vod" || seasonProbe.PlayerType == "zetvideo-vod")
{
// Відфільтровуємо прем'єрні епізоди (ще не вийшли) перед додаванням у структуру
var seasonAvailable = seasonEpisodes.Where(e => !e.IsPremiere).ToList();
int filteredCount = seasonEpisodes.Count - seasonAvailable.Count;
if (filteredCount > 0)
_onLog($"GetSeasonStructure: Відфільтровано {filteredCount} прем'єрних епізодів із сезону {season}");
if (seasonAvailable.Count == 0)
{
_onLog($"GetSeasonStructure: Усі епізоди сезону {season} є прем'єрами, повертаю null");
return null;
}
// Створюємо базовий голос (перший плеєр)
AddVodSeasonEpisodes(structure, seasonProbe.PlayerType, season, seasonEpisodes);
AddVodSeasonEpisodes(structure, seasonProbe.PlayerType, season, seasonAvailable);
// Якщо є додаткові zetvideo плеєри — створюємо окремий голос для кожного
if (seasonEpisodes != null && seasonEpisodes.Count > 0)
if (seasonAvailable.Count > 0)
{
var firstEp = seasonEpisodes.FirstOrDefault(e => e.zetvideoIframeUrls != null && e.zetvideoIframeUrls.Count > 1);
var firstEp = seasonAvailable.FirstOrDefault(e => e.zetvideoIframeUrls != null && e.zetvideoIframeUrls.Count > 1);
if (firstEp != null)
{
// Додаткові плеєри починаються з індексу 1
@ -1190,7 +1235,7 @@ namespace LME.Uaflix
string extraVoiceName = GetZetvideoVoiceName(extraIdx);
_onLog($"GetSeasonStructure: створюю додатковий голос '{extraVoiceName}' для zetvideo плеєра #{extraIdx + 1}");
var extraEpisodes = seasonEpisodes
var extraEpisodes = seasonAvailable
.OrderBy(ep => ep.episode)
.Select(ep => new EpisodeInfo
{
@ -1954,16 +1999,31 @@ namespace LME.Uaflix
var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)");
if (match.Success)
{
var ep = new EpisodeLinkInfo
{
url = episodeUrl,
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}",
season = int.Parse(match.Groups[1].Value),
episode = int.Parse(match.Groups[2].Value)
};
// Перевірка на «Прем'єра» — епізод ще не вийшов
var viDesc = episodeNode.SelectSingleNode(".//div[contains(@class, 'vi-desc')]");
if (viDesc != null)
{
var viTitle = viDesc.SelectSingleNode(".//div[contains(@class, 'vi-title')]");
string descText = viTitle?.InnerText?.Trim() ?? string.Empty;
if (descText.IndexOf("Прем'єра", StringComparison.OrdinalIgnoreCase) >= 0)
{
allEpisodes.Add(new EpisodeLinkInfo
{
url = episodeUrl,
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}",
season = int.Parse(match.Groups[1].Value),
episode = int.Parse(match.Groups[2].Value)
});
ep.IsPremiere = true;
_onLog($"GetPaginationInfo: Серія {ep.episode} позначена як прем'єра: '{descText}'");
}
}
allEpisodes.Add(ep);
}
}
}
}