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) if (isSerial)
{ {
var seasonNumbers = voices.Values bool restrictByVoice = !string.IsNullOrEmpty(t) && voices.TryGetValue(t, out var voiceForSeasons);
.SelectMany(v => v.Seasons.Keys) var seasonNumbers = restrictByVoice
.Distinct() ? GetSeasonSet(voiceForSeasons).OrderBy(n => n).ToList()
.OrderBy(n => n) : voices.Values
.ToList(); .SelectMany(v => GetSeasonSet(v))
.Distinct()
.OrderBy(n => n)
.ToList();
if (seasonNumbers.Count == 0) if (seasonNumbers.Count == 0)
return OnError("mikai", _proxyManager); return OnError("mikai", _proxyManager);
@ -72,6 +75,8 @@ namespace Mikai.Controllers
foreach (var seasonNumber in seasonNumbers) 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}"; 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()); seasonTpl.Append($"{seasonNumber}", link, seasonNumber.ToString());
} }
@ -89,16 +94,29 @@ namespace Mikai.Controllers
if (string.IsNullOrEmpty(t)) if (string.IsNullOrEmpty(t))
t = voicesForSeason[0].Key; t = voicesForSeason[0].Key;
else if (!voices.ContainsKey(t))
t = voicesForSeason[0].Key;
var voiceTpl = new VoiceTpl(); var voiceTpl = new VoiceTpl();
var selectedVoiceInfo = voices[t];
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
foreach (var voice in voicesForSeason) 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); voiceTpl.Append(voice.Key, voice.Key == t, voiceLink);
} }
if (!voices.ContainsKey(t) || !voices[t].Seasons.ContainsKey(s)) 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(); var episodeTpl = new EpisodeTpl();
foreach (var ep in voices[t].Seasons[s].OrderBy(e => e.Number)) 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); 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) private static string StripLampacArgs(string url)
{ {
if (string.IsNullOrEmpty(url)) if (string.IsNullOrEmpty(url))

View File

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

View File

@ -167,11 +167,21 @@ namespace Uaflix.Controllers
// s == -1: Вибір сезону // s == -1: Вибір сезону
if (s == -1) if (s == -1)
{ {
var allSeasons = structure.Voices List<int> allSeasons;
.SelectMany(v => v.Value.Seasons.Keys) bool restrictByVoice = !string.IsNullOrEmpty(t) && structure.Voices.TryGetValue(t, out var tVoice) && IsAshdiVoice(tVoice);
.Distinct() if (restrictByVoice)
.OrderBy(sn => sn) {
.ToList(); 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)}"); OnLog($"Found {allSeasons.Count} seasons in structure: {string.Join(", ", allSeasons)}");
@ -205,6 +215,8 @@ namespace Uaflix.Controllers
foreach (var season in seasonsWithValidEpisodes) 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)}"; 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()); season_tpl.Append($"{season}", link, season.ToString());
OnLog($"Added season {season} to template"); OnLog($"Added season {season} to template");
} }
@ -239,12 +251,31 @@ namespace Uaflix.Controllers
t = voicesForSeason[0].DisplayName; t = voicesForSeason[0].DisplayName;
OnLog($"Auto-selected first voice: {t}"); 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 з усіма озвучками // Створюємо VoiceTpl з усіма озвучками
var voice_tpl = new VoiceTpl(); var voice_tpl = new VoiceTpl();
var selectedVoiceInfo = structure.Voices[t];
var selectedSeasonSet = GetSeasonSet(selectedVoiceInfo);
bool selectedIsAshdi = IsAshdiVoice(selectedVoiceInfo);
foreach (var voice in voicesForSeason) 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; bool isActive = voice.DisplayName == t;
voice_tpl.Append(voice.DisplayName, isActive, voiceLink); voice_tpl.Append(voice.DisplayName, isActive, voiceLink);
} }
@ -261,6 +292,13 @@ namespace Uaflix.Controllers
if (!structure.Voices[t].Seasons.ContainsKey(s)) if (!structure.Voices[t].Seasons.ContainsKey(s))
{ {
OnLog($"Season {s} not found for voice '{t}'"); 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 ==="); OnLog("=== RETURN: season not found for voice OnError ===");
return OnError("uaflix", proxyManager); return OnError("uaflix", proxyManager);
} }
@ -369,5 +407,24 @@ namespace Uaflix.Controllers
cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&'); cleaned = cleaned.Replace("?&", "?").Replace("&&", "&").TrimEnd('?', '&');
return cleaned; 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 class ModInit
{ {
public static double Version => 3.3; public static double Version => 3.4;
public static OnlinesSettings UaFlix; public static OnlinesSettings UaFlix;
public static bool ApnHostProvided; public static bool ApnHostProvided;