feat(mikai,uaflix): implement voice-specific season filtering and redirect handling

Add support for restricting season lists by specific voice selection,
implement proper redirect handling when selected season is unavailable
for chosen voice, and add season set validation to ensure consistent
navigation between voices with different season availability
This commit is contained in:
baliasnyifeliks 2026-02-04 21:52:22 +02:00
parent 6ebd793e90
commit f2b70fa95e
4 changed files with 102 additions and 16 deletions

View File

@ -57,11 +57,14 @@ namespace Mikai.Controllers
if (isSerial)
{
var seasonNumbers = voices.Values
.SelectMany(v => v.Seasons.Keys)
.Distinct()
.OrderBy(n => n)
.ToList();
bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out var voiceForSeasons);
var seasonNumbers = restrictByVoice
? GetSeasonSet(voiceForSeasons).OrderBy(n => n).ToList()
: voices.Values
.SelectMany(v => GetSeasonSet(v))
.Distinct()
.OrderBy(n => n)
.ToList();
if (seasonNumbers.Count == 0)
return OnError("mikai", _proxyManager);
@ -72,6 +75,8 @@ namespace Mikai.Controllers
foreach (var seasonNumber in seasonNumbers)
{
string link = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={seasonNumber}";
if (restrictByVoice)
link += $"&t={HttpUtility.UrlEncode(t)}";
seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
}
@ -89,16 +94,29 @@ namespace Mikai.Controllers
if (string.IsNullOrEmpty(t))
t = voicesForSeason[0].Key;
else if (!voices.ContainsKey(t))
t = voicesForSeason[0].Key;
var voiceTpl = new VoiceTpl();
var selectedVoiceInfo = voices[t];
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
foreach (var voice in voicesForSeason)
{
string voiceLink = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
var targetSeasonSet = GetSeasonSet(voice.Value);
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
string voiceLink = $"{host}/mikai?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1";
if (sameSeasonSet)
voiceLink += $"&s={s}&t={HttpUtility.UrlEncode(voice.Key)}";
else
voiceLink += $"&s=-1&t={HttpUtility.UrlEncode(voice.Key)}";
voiceTpl.Append(voice.Key, voice.Key == t, voiceLink);
}
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s))
return OnError("mikai", _proxyManager);
{
string redirectUrl = $"{host}/mikai?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)}";
return Redirect(redirectUrl);
}
var episodeTpl = new EpisodeTpl();
foreach (var ep in voices[t].Seasons[s].OrderBy(e => e.Number))
@ -367,6 +385,17 @@ namespace Mikai.Controllers
streamLink.Contains("moonanime.art", StringComparison.OrdinalIgnoreCase);
}
private static HashSet<int> GetSeasonSet(MikaiVoiceInfo 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.Url)))
.Select(kv => kv.Key)
.ToHashSet();
}
private static string StripLampacArgs(string url)
{
if (string.IsNullOrEmpty(url))

View File

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

View File

@ -167,11 +167,21 @@ namespace Uaflix.Controllers
// s == -1: Вибір сезону
if (s == -1)
{
var allSeasons = structure.Voices
.SelectMany(v => v.Value.Seasons.Keys)
.Distinct()
.OrderBy(sn => sn)
.ToList();
List<int> allSeasons;
bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out var 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();
}
OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
@ -205,6 +215,8 @@ namespace Uaflix.Controllers
foreach (var season in seasonsWithValidEpisodes)
{
string link = $"{host}/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)
link += $"&t={HttpUtility.UrlEncode(t)}";
season_tpl.Append($"{season}", link, season.ToString());
OnLog($"Added season {season} to template");
}
@ -239,12 +251,31 @@ namespace Uaflix.Controllers
t = voicesForSeason[0].DisplayName;
OnLog($"Auto-selected first voice: {t}");
}
else if (!structure.Voices.ContainsKey(t))
{
t = voicesForSeason[0].DisplayName;
OnLog($"Voice '{t}' not found, fallback to first voice: {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)
{
string voiceLink = $"{host}/uaflix?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&t={HttpUtility.UrlEncode(voice.DisplayName)}&href={HttpUtility.UrlEncode(filmUrl)}";
bool targetIsAshdi = IsAshdiVoice(voice.Info);
var targetSeasonSet = GetSeasonSet(voice.Info);
bool sameSeasonSet = targetSeasonSet.SetEquals(selectedSeasonSet);
bool needSeasonReset = (selectedIsAshdi || targetIsAshdi) && !sameSeasonSet;
string voiceLink = $"{host}/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)}";
bool isActive = voice.DisplayName == t;
voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
}
@ -261,6 +292,13 @@ namespace Uaflix.Controllers
if (!structure.Voices[t].Seasons.ContainsKey(s))
{
OnLog($"Season {s} not found for voice '{t}'");
if (IsAshdiVoice(structure.Voices[t]))
{
string redirectUrl = $"{host}/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", proxyManager);
}
@ -369,5 +407,24 @@ namespace Uaflix.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
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();
}
}
}

View File

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