diff --git a/LME.Uaflix/Controller.cs b/LME.Uaflix/Controller.cs
index 065bb5d..c9588a2 100644
--- a/LME.Uaflix/Controller.cs
+++ b/LME.Uaflix/Controller.cs
@@ -76,8 +76,10 @@ namespace LME.Uaflix.Controllers
if (play)
{
- // Визначаємо URL для парсингу - або з параметра t, або з episode_url
- string urlToParse = !string.IsNullOrEmpty(t) ? t : Request.Query["episode_url"];
+ // Визначаємо URL для парсингу (параметр t тепер може бути назвою голосу, а не URL)
+ string urlToParse = Request.Query["episode_url"];
+ if (string.IsNullOrWhiteSpace(urlToParse))
+ urlToParse = t;
if (string.IsNullOrWhiteSpace(urlToParse))
{
OnLog("=== RETURN: play missing url OnError ===");
@@ -87,8 +89,21 @@ namespace LME.Uaflix.Controllers
var playResult = await invoke.ParseEpisode(urlToParse);
if (playResult.streams != null && playResult.streams.Count > 0)
{
- OnLog("=== RETURN: play redirect ===");
- return UpdateService.Validate(Redirect(BuildStreamUrl(init, playResult.streams.First().link)));
+ // Якщо кілька потоків, вибираємо за голосом
+ PlayStream targetStream;
+ if (playResult.streams.Count > 1 && !string.IsNullOrEmpty(t))
+ {
+ targetStream = playResult.streams.FirstOrDefault(s =>
+ string.Equals(s.title, t, StringComparison.OrdinalIgnoreCase))
+ ?? playResult.streams.First();
+ }
+ else
+ {
+ targetStream = playResult.streams.First();
+ }
+
+ OnLog($"=== RETURN: play redirect (stream: {targetStream.title}) ===");
+ return UpdateService.Validate(Redirect(BuildStreamUrl(init, targetStream.link)));
}
OnLog("=== RETURN: play no streams ===");
@@ -102,9 +117,28 @@ namespace LME.Uaflix.Controllers
var playResult = await invoke.ParseEpisode(episodeUrl);
if (playResult.streams != null && playResult.streams.Count > 0)
{
+ // Якщо є кілька потоків (напр. Uaflix + Оригінал), вибираємо за голосом (t)
+ PlayStream targetStream;
+ if (playResult.streams.Count > 1 && !string.IsNullOrEmpty(t))
+ {
+ targetStream = playResult.streams.FirstOrDefault(s =>
+ string.Equals(s.title, t, StringComparison.OrdinalIgnoreCase));
+ if (targetStream == null)
+ {
+ _onLog($"call method: голос '{t}' не знайдено серед потоків, використовую перший");
+ targetStream = playResult.streams.First();
+ }
+ else
+ _onLog($"call method: вибрано потік для голосу '{t}'");
+ }
+ else
+ {
+ targetStream = playResult.streams.First();
+ }
+
// Повертаємо JSON з інформацією про стрім для методу 'play'
- string streamUrl = BuildStreamUrl(init, playResult.streams.First().link);
- var subtitles = playResult.subtitles ?? playResult.streams.FirstOrDefault(s => s.subtitles != null)?.subtitles;
+ string streamUrl = BuildStreamUrl(init, targetStream.link);
+ var subtitles = playResult.subtitles ?? targetStream.subtitles;
OnLog($"=== RETURN: call method JSON for episode_url ===");
return UpdateService.Validate(Content(VideoTpl.ToJson("play", streamUrl, title ?? original_title, subtitles: subtitles), "application/json; charset=utf-8"));
@@ -277,7 +311,7 @@ namespace LME.Uaflix.Controllers
{
// Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику
// Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true
- string callUrl = $"{host}/lite/lme_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}";
+ string callUrl = $"{host}/lite/lme_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}&t={HttpUtility.UrlEncode(t ?? "Uaflix")}";
episode_tpl.Append(
name: episodeTitle,
title: title,
diff --git a/LME.Uaflix/Models/EpisodeLinkInfo.cs b/LME.Uaflix/Models/EpisodeLinkInfo.cs
index d21c791..eb4eedb 100644
--- a/LME.Uaflix/Models/EpisodeLinkInfo.cs
+++ b/LME.Uaflix/Models/EpisodeLinkInfo.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
namespace LME.Uaflix.Models
{
@@ -12,5 +13,11 @@ namespace LME.Uaflix.Models
// Нові поля для підтримки змішаних плеєрів
public string playerType { get; set; } // "ashdi-serial", "zetvideo-serial", "zetvideo-vod", "ashdi-vod"
public string iframeUrl { get; set; } // URL iframe для цього епізоду
+
+ ///
+ /// Всі zetvideo iframe URL на сторінці епізоду (для створення кількох перекладів)
+ /// Перший елемент відповідає iframeUrl, наступні — додаткові плеєри (напр. з субтитрами)
+ ///
+ public List zetvideoIframeUrls { get; set; }
}
}
\ No newline at end of file
diff --git a/LME.Uaflix/UaflixInvoke.cs b/LME.Uaflix/UaflixInvoke.cs
index fc7b43f..676cd22 100644
--- a/LME.Uaflix/UaflixInvoke.cs
+++ b/LME.Uaflix/UaflixInvoke.cs
@@ -286,14 +286,14 @@ namespace LME.Uaflix
return result;
}
- private async Task<(string iframeUrl, string playerType)> ProbeEpisodePlayer(string pageUrl)
+ private async Task ProbeEpisodePlayer(string pageUrl)
{
if (string.IsNullOrWhiteSpace(pageUrl))
- return (null, null);
+ return null;
string memKey = $"lme_uaflix:episode-player:{pageUrl}";
if (_hybridCache.TryGetValue(memKey, out EpisodePlayerInfo cached))
- return (cached?.IframeUrl, cached?.PlayerType);
+ return cached;
try
{
@@ -305,7 +305,7 @@ namespace LME.Uaflix
string html = await GetHtml(pageUrl, headers);
if (string.IsNullOrWhiteSpace(html))
- return (null, null);
+ return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
@@ -313,25 +313,40 @@ namespace LME.Uaflix
string iframeUrl = ExtractIframeUrl(doc);
string playerType = DeterminePlayerType(iframeUrl);
- _hybridCache.Set(memKey, new EpisodePlayerInfo
+ // Витягуємо всі zetvideo iframe для підтримки кількох перекладів
+ List zetvideoIframeUrls = null;
+ if (playerType == "zetvideo-vod")
+ {
+ var allIframes = ExtractAllIframeUrls(doc);
+ var zetIframes = allIframes
+ .Where(u => u != null && u.Contains("zetvideo.net"))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ if (zetIframes.Count > 1)
+ zetvideoIframeUrls = zetIframes;
+ }
+
+ var info = new EpisodePlayerInfo
{
IframeUrl = iframeUrl,
- PlayerType = playerType
- }, cacheTime(20));
+ PlayerType = playerType,
+ ZetvideoIframeUrls = zetvideoIframeUrls
+ };
- return (iframeUrl, playerType);
+ _hybridCache.Set(memKey, info, cacheTime(20));
+ return info;
}
catch (Exception ex)
{
_onLog($"ProbeEpisodePlayer error ({pageUrl}): {ex.Message}");
- return (null, null);
+ return null;
}
}
- private async Task<(string iframeUrl, string playerType)> ProbeSeasonPlayer(List seasonEpisodes)
+ private async Task ProbeSeasonPlayer(List seasonEpisodes)
{
if (seasonEpisodes == null || seasonEpisodes.Count == 0)
- return (null, null);
+ return null;
foreach (var episode in seasonEpisodes.OrderBy(e => e.episode))
{
@@ -339,21 +354,26 @@ namespace LME.Uaflix
continue;
var probed = await ProbeEpisodePlayer(episode.url);
- string playerType = probed.playerType;
-
- episode.iframeUrl = probed.iframeUrl;
- episode.playerType = playerType;
-
- if (string.IsNullOrWhiteSpace(playerType))
+ if (probed == null)
continue;
- if (playerType == "trailer")
+ episode.iframeUrl = probed.IframeUrl;
+ episode.playerType = probed.PlayerType;
+
+ // Зберігаємо всі zetvideo iframe для створення кількох перекладів
+ if (probed.ZetvideoIframeUrls != null && probed.ZetvideoIframeUrls.Count > 0)
+ episode.zetvideoIframeUrls = probed.ZetvideoIframeUrls;
+
+ if (string.IsNullOrWhiteSpace(probed.PlayerType))
+ continue;
+
+ if (probed.PlayerType == "trailer")
continue;
return probed;
}
- return (null, null);
+ return null;
}
private static string NormalizeSerialPlayerKey(string playerType, string iframeUrl)
@@ -732,22 +752,22 @@ namespace LME.Uaflix
_onLog($"AggregateSerialStructure: Processing season {season}");
var seasonProbe = await ProbeSeasonPlayer(seasonGroup.Value);
- if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
+ if (seasonProbe == null || string.IsNullOrWhiteSpace(seasonProbe.PlayerType))
{
_onLog($"AggregateSerialStructure: Season {season} has no supported player");
continue;
}
- if (seasonProbe.playerType == "ashdi-serial" || seasonProbe.playerType == "zetvideo-serial")
+ if (seasonProbe.PlayerType == "ashdi-serial" || seasonProbe.PlayerType == "zetvideo-serial")
{
- string serialKey = NormalizeSerialPlayerKey(seasonProbe.playerType, seasonProbe.iframeUrl);
+ string serialKey = NormalizeSerialPlayerKey(seasonProbe.PlayerType, seasonProbe.IframeUrl);
if (!serialPlayersProcessed.Add(serialKey))
{
_onLog($"AggregateSerialStructure: Serial player already parsed for season {season}: {serialKey}");
continue;
}
- var voices = await ParseMultiEpisodePlayer(seasonProbe.iframeUrl, seasonProbe.playerType);
+ var voices = await ParseMultiEpisodePlayer(seasonProbe.IframeUrl, seasonProbe.PlayerType);
if (voices == null || voices.Count == 0)
{
_onLog($"AggregateSerialStructure: No voices in serial player for season {season}");
@@ -755,18 +775,18 @@ namespace LME.Uaflix
}
MergeVoices(structure, voices);
- _onLog($"AggregateSerialStructure: Parsed serial player {seasonProbe.playerType}, voices={voices.Count}");
+ _onLog($"AggregateSerialStructure: Parsed serial player {seasonProbe.PlayerType}, voices={voices.Count}");
continue;
}
- if (seasonProbe.playerType == "ashdi-vod" || seasonProbe.playerType == "zetvideo-vod")
+ if (seasonProbe.PlayerType == "ashdi-vod" || seasonProbe.PlayerType == "zetvideo-vod")
{
- AddVodSeasonEpisodes(structure, seasonProbe.playerType, season, seasonGroup.Value);
+ AddVodSeasonEpisodes(structure, seasonProbe.PlayerType, season, seasonGroup.Value);
_onLog($"AggregateSerialStructure: Added vod season {season}, episodes={seasonGroup.Value.Count}");
continue;
}
- _onLog($"AggregateSerialStructure: Unsupported player {seasonProbe.playerType} for season {season}");
+ _onLog($"AggregateSerialStructure: Unsupported player {seasonProbe.PlayerType} for season {season}");
}
}
else
@@ -774,15 +794,15 @@ namespace LME.Uaflix
_onLog($"AggregateSerialStructure: No episodes from pagination for {serialUrl}, fallback to page iframe");
var serialProbe = await ProbeEpisodePlayer(serialUrl);
- if (string.IsNullOrWhiteSpace(serialProbe.playerType))
+ if (serialProbe == null || string.IsNullOrWhiteSpace(serialProbe.PlayerType))
{
_onLog($"AggregateSerialStructure: Fallback probe failed for {serialUrl}");
return null;
}
- if (serialProbe.playerType == "ashdi-serial" || serialProbe.playerType == "zetvideo-serial")
+ if (serialProbe.PlayerType == "ashdi-serial" || serialProbe.PlayerType == "zetvideo-serial")
{
- var voices = await ParseMultiEpisodePlayer(serialProbe.iframeUrl, serialProbe.playerType);
+ var voices = await ParseMultiEpisodePlayer(serialProbe.IframeUrl, serialProbe.PlayerType);
if (voices == null || voices.Count == 0)
{
_onLog($"AggregateSerialStructure: Fallback serial player has no voices for {serialUrl}");
@@ -792,8 +812,13 @@ namespace LME.Uaflix
MergeVoices(structure, voices);
_onLog($"AggregateSerialStructure: Fallback serial player parsed, voices={voices.Count}");
}
- else if (serialProbe.playerType == "ashdi-vod" || serialProbe.playerType == "zetvideo-vod")
+ else if (serialProbe.PlayerType == "ashdi-vod" || serialProbe.PlayerType == "zetvideo-vod")
{
+ // Копіюємо zetvideoIframeUrls якщо є
+ List zetvideoUrls = null;
+ if (serialProbe.ZetvideoIframeUrls != null && serialProbe.ZetvideoIframeUrls.Count > 0)
+ zetvideoUrls = serialProbe.ZetvideoIframeUrls;
+
var syntheticEpisodes = new List
{
new EpisodeLinkInfo
@@ -802,17 +827,18 @@ namespace LME.Uaflix
title = "Епізод 1",
season = 1,
episode = 1,
- iframeUrl = serialProbe.iframeUrl,
- playerType = serialProbe.playerType
+ iframeUrl = serialProbe.IframeUrl,
+ playerType = serialProbe.PlayerType,
+ zetvideoIframeUrls = zetvideoUrls
}
};
structure.AllEpisodes = syntheticEpisodes;
- AddVodSeasonEpisodes(structure, serialProbe.playerType, 1, syntheticEpisodes);
+ AddVodSeasonEpisodes(structure, serialProbe.PlayerType, 1, syntheticEpisodes);
}
else
{
- _onLog($"AggregateSerialStructure: Fallback player is not supported for serial: {serialProbe.playerType}");
+ _onLog($"AggregateSerialStructure: Fallback player is not supported for serial: {serialProbe.PlayerType}");
return null;
}
}
@@ -1096,20 +1122,20 @@ namespace LME.Uaflix
};
var seasonProbe = await ProbeSeasonPlayer(seasonEpisodes);
- if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
+ if (seasonProbe == null || string.IsNullOrWhiteSpace(seasonProbe.PlayerType))
{
// fallback: інколи плеєр є лише на головній сторінці
seasonProbe = await ProbeEpisodePlayer(serialUrl);
- if (string.IsNullOrWhiteSpace(seasonProbe.playerType))
+ if (seasonProbe == null || string.IsNullOrWhiteSpace(seasonProbe.PlayerType))
{
_onLog($"GetSeasonStructure: unsupported player for season={season}");
return null;
}
}
- if (seasonProbe.playerType == "ashdi-serial" || seasonProbe.playerType == "zetvideo-serial")
+ if (seasonProbe.PlayerType == "ashdi-serial" || seasonProbe.PlayerType == "zetvideo-serial")
{
- var voices = await ParseMultiEpisodePlayerCached(seasonProbe.iframeUrl, seasonProbe.playerType);
+ var voices = await ParseMultiEpisodePlayerCached(seasonProbe.IframeUrl, seasonProbe.PlayerType);
foreach (var voice in voices)
{
if (voice?.Seasons == null || !voice.Seasons.TryGetValue(season, out List seasonVoiceEpisodes) || seasonVoiceEpisodes == null || seasonVoiceEpisodes.Count == 0)
@@ -1138,13 +1164,53 @@ namespace LME.Uaflix
};
}
}
- else if (seasonProbe.playerType == "ashdi-vod" || seasonProbe.playerType == "zetvideo-vod")
+ else if (seasonProbe.PlayerType == "ashdi-vod" || seasonProbe.PlayerType == "zetvideo-vod")
{
- AddVodSeasonEpisodes(structure, seasonProbe.playerType, season, seasonEpisodes);
+ // Створюємо базовий голос (перший плеєр)
+ AddVodSeasonEpisodes(structure, seasonProbe.PlayerType, season, seasonEpisodes);
+
+ // Якщо є додаткові zetvideo плеєри — створюємо окремий голос для кожного
+ if (seasonEpisodes != null && seasonEpisodes.Count > 0)
+ {
+ var firstEp = seasonEpisodes.FirstOrDefault(e => e.zetvideoIframeUrls != null && e.zetvideoIframeUrls.Count > 1);
+ if (firstEp != null)
+ {
+ // Додаткові плеєри починаються з індексу 1
+ for (int extraIdx = 1; extraIdx < firstEp.zetvideoIframeUrls.Count; extraIdx++)
+ {
+ string extraVoiceName = GetZetvideoVoiceName(extraIdx);
+ _onLog($"GetSeasonStructure: створюю додатковий голос '{extraVoiceName}' для zetvideo плеєра #{extraIdx + 1}");
+
+ var extraEpisodes = seasonEpisodes
+ .OrderBy(ep => ep.episode)
+ .Select(ep => new EpisodeInfo
+ {
+ Number = ep.episode,
+ Title = ep.title,
+ File = ep.url,
+ Id = ep.url,
+ Poster = null,
+ Subtitle = null
+ })
+ .ToList();
+
+ structure.Voices[extraVoiceName] = new VoiceInfo
+ {
+ Name = extraVoiceName,
+ PlayerType = seasonProbe.PlayerType,
+ DisplayName = extraVoiceName,
+ Seasons = new Dictionary>
+ {
+ [season] = extraEpisodes
+ }
+ };
+ }
+ }
+ }
}
else
{
- _onLog($"GetSeasonStructure: player '{seasonProbe.playerType}' is not supported");
+ _onLog($"GetSeasonStructure: player '{seasonProbe.PlayerType}' is not supported");
return null;
}
@@ -2056,6 +2122,15 @@ namespace LME.Uaflix
}
}
+ ///
+ /// Повертає назву голосу для додаткового zetvideo плеєра за індексом
+ /// Індекс 0 = "Uaflix" (основний), індекс 1 = "Оригінал", індекс 2+ = "Оригінал #N"
+ ///
+ private static string GetZetvideoVoiceName(int playerIndex)
+ {
+ return playerIndex <= 1 ? "Оригінал" : $"Оригінал #{playerIndex}";
+ }
+
async Task> ParseAllZetvideoSources(string iframeUrl)
{
var result = new List();
@@ -2313,6 +2388,7 @@ namespace LME.Uaflix
{
public string IframeUrl { get; set; }
public string PlayerType { get; set; }
+ public List ZetvideoIframeUrls { get; set; }
}
sealed class SearchMeta