chore: remove deprecated modules and files

- Remove AnimeON module and related files
- Remove Bamboo module and related files
- Remove CikavaIdeya module and related files
- Remove StarLight module and related files
- Remove UAKino module and related files
- Remove UaTUT module and related files
- Remove Uaflix module and related files
- Remove Unimay module and related files
- Remove LICENSE and README.md files
- Remove .gitignore file
This commit is contained in:
baliasnyifeliks 2026-01-14 13:49:25 +02:00
parent b139444cba
commit 7e7c8c9659
74 changed files with 0 additions and 7494 deletions

9
.gitignore vendored
View File

@ -1,9 +0,0 @@
/.idea/
/AIDocumentation/
/LampaC/
/BanderaBackend/
/Kinovezha/
/.clinerules/moduls.md
/.clinerules/uaflix-optimization.md
/.clinerules/
/.qodo/

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,352 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models;
using System.Text.Json;
using System.Linq;
using System.Text;
using AnimeON.Models;
using Shared.Engine;
namespace AnimeON
{
public class AnimeONInvoke
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private OnlinesSettings _init;
private HybridCache _hybridCache;
private Action<string> _onLog;
private ProxyManager _proxyManager;
public AnimeONInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<SearchModel>> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year)
{
string memKey = $"AnimeON:search:{kinopoisk_id}:{imdb_id}";
if (_hybridCache.TryGetValue(memKey, out List<SearchModel> res))
return res;
try
{
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) };
async Task<List<SearchModel>> FindAnime(string query)
{
if (string.IsNullOrEmpty(query))
return null;
string searchUrl = $"{_init.host}/api/anime/search?text={System.Web.HttpUtility.UrlEncode(query)}";
if (IsNotAllowedHost(searchUrl))
return null;
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {searchUrl}");
string searchJson = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(searchJson))
return null;
var searchResponse = JsonSerializer.Deserialize<SearchResponseModel>(searchJson);
return searchResponse?.Result;
}
var searchResults = await FindAnime(title) ?? await FindAnime(original_title);
if (searchResults == null)
return null;
if (!string.IsNullOrEmpty(imdb_id))
{
var seasons = searchResults.Where(a => a.ImdbId == imdb_id).ToList();
if (seasons.Count > 0)
{
_hybridCache.Set(memKey, seasons, cacheTime(5));
return seasons;
}
}
// Fallback to first result if no imdb match
var firstResult = searchResults.FirstOrDefault();
if (firstResult != null)
{
var list = new List<SearchModel> { firstResult };
_hybridCache.Set(memKey, list, cacheTime(5));
return list;
}
return null;
}
catch (Exception ex)
{
_onLog($"AnimeON error: {ex.Message}");
}
return null;
}
public async Task<List<FundubModel>> GetFundubs(int animeId)
{
string fundubsUrl = $"{_init.host}/api/player/{animeId}/translations";
if (IsNotAllowedHost(fundubsUrl))
return null;
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {fundubsUrl}");
string fundubsJson = await Http.Get(fundubsUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(fundubsJson))
return null;
var fundubsResponse = JsonSerializer.Deserialize<FundubsResponseModel>(fundubsJson);
if (fundubsResponse?.Translations == null || fundubsResponse.Translations.Count == 0)
return null;
var fundubs = new List<FundubModel>();
foreach (var translation in fundubsResponse.Translations)
{
var fundubModel = new FundubModel
{
Fundub = translation.Translation,
Player = translation.Player
};
fundubs.Add(fundubModel);
}
return fundubs;
}
public async Task<EpisodeModel> GetEpisodes(int animeId, int playerId, int fundubId)
{
string episodesUrl = $"{_init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
if (IsNotAllowedHost(episodesUrl))
return null;
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {episodesUrl}");
string episodesJson = await Http.Get(episodesUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(episodesJson))
return null;
return JsonSerializer.Deserialize<EpisodeModel>(episodesJson);
}
public async Task<string> ParseMoonAnimePage(string url)
{
try
{
string requestUrl = $"{url}?player=animeon.club";
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://animeon.club/")
};
if (IsNotAllowedHost(requestUrl))
return null;
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {requestUrl}");
string html = await Http.Get(requestUrl, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
var match = System.Text.RegularExpressions.Regex.Match(html, @"file:\s*""([^""]+\.m3u8)""");
if (match.Success)
{
return match.Groups[1].Value;
}
}
catch (Exception ex)
{
_onLog($"AnimeON ParseMoonAnimePage error: {ex.Message}");
}
return null;
}
public async Task<string> ParseAshdiPage(string url)
{
try
{
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://ashdi.vip/")
};
if (IsNotAllowedHost(url))
return null;
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {url}");
string html = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
var match = System.Text.RegularExpressions.Regex.Match(html, @"file\s*:\s*['""]([^'""]+)['""]");
if (match.Success)
{
return match.Groups[1].Value;
}
}
catch (Exception ex)
{
_onLog($"AnimeON ParseAshdiPage error: {ex.Message}");
}
return null;
}
public async Task<string> ResolveEpisodeStream(int episodeId)
{
try
{
string url = $"{_init.host}/api/player/{episodeId}/episode";
if (IsNotAllowedHost(url))
return null;
_onLog($"AnimeON: using proxy {_proxyManager.CurrentProxyIp} for {url}");
string json = await Http.Get(url, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(json))
return null;
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("fileUrl", out var fileProp))
{
string fileUrl = fileProp.GetString();
if (!string.IsNullOrEmpty(fileUrl))
return fileUrl;
}
if (root.TryGetProperty("videoUrl", out var videoProp))
{
string videoUrl = videoProp.GetString();
return await ResolveVideoUrl(videoUrl);
}
}
catch (Exception ex)
{
_onLog($"AnimeON ResolveEpisodeStream error: {ex.Message}");
}
return null;
}
public async Task<string> ResolveVideoUrl(string url)
{
if (string.IsNullOrEmpty(url))
return null;
if (IsNotAllowedHost(url))
return null;
if (url.Contains("moonanime.art"))
return await ParseMoonAnimePage(url);
if (url.Contains("ashdi.vip/vod"))
return await ParseAshdiPage(url);
return url;
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
public async Task<AnimeON.Models.AnimeONAggregatedStructure> AggregateSerialStructure(int animeId, int season)
{
string memKey = $"AnimeON:aggregated:{animeId}:{season}";
if (_hybridCache.TryGetValue(memKey, out AnimeON.Models.AnimeONAggregatedStructure cached))
return cached;
try
{
var structure = new AnimeON.Models.AnimeONAggregatedStructure
{
AnimeId = animeId,
Season = season,
Voices = new Dictionary<string, AnimeON.Models.AnimeONVoiceInfo>()
};
var fundubs = await GetFundubs(animeId);
if (fundubs == null || fundubs.Count == 0)
return null;
foreach (var fundub in fundubs)
{
if (fundub?.Fundub == null || fundub.Player == null)
continue;
foreach (var player in fundub.Player)
{
string display = $"[{player.Name}] {fundub.Fundub.Name}";
var episodesData = await GetEpisodes(animeId, player.Id, fundub.Fundub.Id);
if (episodesData?.Episodes == null || episodesData.Episodes.Count == 0)
continue;
var voiceInfo = new AnimeON.Models.AnimeONVoiceInfo
{
Name = fundub.Fundub.Name,
PlayerType = player.Name?.ToLower(),
DisplayName = display,
PlayerId = player.Id,
FundubId = fundub.Fundub.Id,
Episodes = episodesData.Episodes
.OrderBy(ep => ep.EpisodeNum)
.Select(ep => new AnimeON.Models.AnimeONEpisodeInfo
{
Number = ep.EpisodeNum,
Title = ep.Name,
Hls = ep.Hls,
VideoUrl = ep.VideoUrl,
EpisodeId = ep.Id
})
.ToList()
};
structure.Voices[display] = voiceInfo;
}
}
if (!structure.Voices.Any())
return null;
_hybridCache.Set(memKey, structure, cacheTime(20, init: _init));
return structure;
}
catch (Exception ex)
{
_onLog?.Invoke($"AnimeON AggregateSerialStructure error: {ex.Message}");
return null;
}
}
}
}

View File

@ -1,361 +0,0 @@
using System.Text.Json;
using Shared.Engine;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using Shared;
using Shared.Models.Templates;
using AnimeON.Models;
using System.Text.RegularExpressions;
using System.Text;
using Shared.Models.Online.Settings;
using Shared.Models;
using HtmlAgilityPack;
namespace AnimeON.Controllers
{
public class Controller : BaseOnlineController
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.AnimeON);
}
[HttpGet]
[Route("animeon")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false)
{
var init = await loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
OnLog($"AnimeON Index: title={title}, original_title={original_title}, serial={serial}, s={s}, t={t}, year={year}, imdb_id={imdb_id}, kp={kinopoisk_id}");
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year);
OnLog($"AnimeON: search results = {seasons?.Count ?? 0}");
if (seasons == null || seasons.Count == 0)
return OnError("animeon", proxyManager);
// [Refactoring] Використовується агрегована структура (AggregateSerialStructure) — попередній збір allOptions не потрібний
// [Refactoring] Перевірка allOptions видалена — використовується перевірка структури озвучок нижче
if (serial == 1)
{
if (s == -1) // Крок 1: Вибір аніме (як сезони)
{
var season_tpl = new SeasonTpl(seasons.Count);
for (int i = 0; i < seasons.Count; i++)
{
var anime = seasons[i];
string seasonName = anime.Season.ToString();
string link = $"{host}/animeon?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}";
season_tpl.Append(seasonName, link, anime.Season.ToString());
}
OnLog($"AnimeON: return seasons count={seasons.Count}");
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
else // Крок 2/3: Вибір озвучки та епізодів
{
if (s >= seasons.Count)
return OnError("animeon", proxyManager);
var selectedAnime = seasons[s];
var structure = await invoke.AggregateSerialStructure(selectedAnime.Id, selectedAnime.Season);
if (structure == null || !structure.Voices.Any())
return OnError("animeon", proxyManager);
OnLog($"AnimeON: voices found = {structure.Voices.Count}");
// Автовибір першої озвучки якщо t не задано
if (string.IsNullOrEmpty(t))
t = structure.Voices.Keys.First();
// Формуємо список озвучок
var voice_tpl = new VoiceTpl();
foreach (var voice in structure.Voices)
{
string voiceLink = $"{host}/animeon?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)}";
bool isActive = voice.Key == t;
voice_tpl.Append(voice.Key, isActive, voiceLink);
}
// Перевірка вибраної озвучки
if (!structure.Voices.ContainsKey(t))
return OnError("animeon", proxyManager);
var episode_tpl = new EpisodeTpl();
var selectedVoiceInfo = structure.Voices[t];
// Формуємо епізоди для вибраної озвучки
foreach (var ep in selectedVoiceInfo.Episodes.OrderBy(e => e.Number))
{
string episodeName = !string.IsNullOrEmpty(ep.Title) ? ep.Title : $"Епізод {ep.Number}";
string seasonStr = selectedAnime.Season.ToString();
string episodeStr = ep.Number.ToString();
string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl;
bool needsResolve = selectedVoiceInfo.PlayerType == "moon" || selectedVoiceInfo.PlayerType == "ashdi";
if (string.IsNullOrEmpty(streamLink) && ep.EpisodeId > 0)
{
string callUrl = $"{host}/animeon/play?episode_id={ep.EpisodeId}";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
continue;
}
if (string.IsNullOrEmpty(streamLink))
continue;
if (needsResolve || streamLink.Contains("moonanime.art") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, accsArgs(callUrl), "call");
}
else
{
string playUrl = HostStreamProxy(init, accsArgs(streamLink));
episode_tpl.Append(episodeName, title ?? original_title, seasonStr, episodeStr, playUrl);
}
}
// Повертаємо озвучки + епізоди разом
OnLog($"AnimeON: return episodes count={selectedVoiceInfo.Episodes.Count} for voice='{t}' season={selectedAnime.Season}");
if (rjson)
return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8");
return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
else // Фільм
{
var firstAnime = seasons.FirstOrDefault();
if (firstAnime == null)
return OnError("animeon", proxyManager);
var fundubs = await invoke.GetFundubs(firstAnime.Id);
OnLog($"AnimeON: movie fundubs count = {fundubs?.Count ?? 0}");
if (fundubs == null || fundubs.Count == 0)
return OnError("animeon", proxyManager);
var tpl = new MovieTpl(title, original_title);
foreach (var fundub in fundubs)
{
if (fundub?.Fundub == null || fundub.Player == null || fundub.Player.Count == 0)
continue;
foreach (var player in fundub.Player)
{
var episodesData = await invoke.GetEpisodes(firstAnime.Id, player.Id, fundub.Fundub.Id);
if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0)
continue;
var firstEp = episodesData.Episodes.FirstOrDefault();
if (firstEp == null)
continue;
string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl;
if (string.IsNullOrEmpty(streamLink))
continue;
string translationName = $"[{player.Name}] {fundub.Fundub.Name}";
bool needsResolve = player.Name?.ToLower() == "moon" || player.Name?.ToLower() == "ashdi";
if (needsResolve || streamLink.Contains("moonanime.art/iframe/") || streamLink.Contains("ashdi.vip/vod"))
{
string callUrl = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
tpl.Append(translationName, accsArgs(callUrl), "call");
}
else
{
tpl.Append(translationName, HostStreamProxy(init, accsArgs(streamLink)));
}
}
}
// Якщо не зібрали жодної опції — повертаємо помилку
if (tpl.IsEmpty())
return OnError("animeon", proxyManager);
OnLog("AnimeON: return movie options");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
}
}
async Task<List<FundubModel>> GetFundubs(OnlinesSettings init, int animeId)
{
string fundubsUrl = $"{init.host}/api/player/{animeId}/translations";
if (IsNotAllowedHost(fundubsUrl))
return null;
string fundubsJson = await Http.Get(fundubsUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(fundubsJson))
return null;
var fundubsResponse = JsonSerializer.Deserialize<FundubsResponseModel>(fundubsJson);
if (fundubsResponse?.Translations == null || fundubsResponse.Translations.Count == 0)
return null;
var fundubs = new List<FundubModel>();
foreach (var translation in fundubsResponse.Translations)
{
var fundubModel = new FundubModel
{
Fundub = translation.Translation,
Player = translation.Player
};
fundubs.Add(fundubModel);
}
return fundubs;
}
async Task<EpisodeModel> GetEpisodes(OnlinesSettings init, int animeId, int playerId, int fundubId)
{
string episodesUrl = $"{init.host}/api/player/{animeId}/episodes?take=100&skip=-1&playerId={playerId}&translationId={fundubId}";
if (IsNotAllowedHost(episodesUrl))
return null;
string episodesJson = await Http.Get(episodesUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
if (string.IsNullOrEmpty(episodesJson))
return null;
return JsonSerializer.Deserialize<EpisodeModel>(episodesJson);
}
async ValueTask<List<SearchModel>> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year)
{
string memKey = $"AnimeON:search:{kinopoisk_id}:{imdb_id}";
if (hybridCache.TryGetValue(memKey, out List<SearchModel> res))
return res;
try
{
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) };
async Task<List<SearchModel>> FindAnime(string query)
{
if (string.IsNullOrEmpty(query))
return null;
string searchUrl = $"{init.host}/api/anime/search?text={HttpUtility.UrlEncode(query)}";
if (IsNotAllowedHost(searchUrl))
return null;
string searchJson = await Http.Get(searchUrl, headers: headers);
if (string.IsNullOrEmpty(searchJson))
return null;
var searchResponse = JsonSerializer.Deserialize<SearchResponseModel>(searchJson);
return searchResponse?.Result;
}
var searchResults = await FindAnime(title) ?? await FindAnime(original_title);
if (searchResults == null)
return null;
if (!string.IsNullOrEmpty(imdb_id))
{
var seasons = searchResults.Where(a => a.ImdbId == imdb_id).ToList();
if (seasons.Count > 0)
{
hybridCache.Set(memKey, seasons, cacheTime(5));
return seasons;
}
}
// Fallback to first result if no imdb match
var firstResult = searchResults.FirstOrDefault();
if (firstResult != null)
{
var list = new List<SearchModel> { firstResult };
hybridCache.Set(memKey, list, cacheTime(5));
return list;
}
return null;
}
catch (Exception ex)
{
OnLog($"AnimeON error: {ex.Message}");
}
return null;
}
[HttpGet("animeon/play")]
public async Task<ActionResult> Play(string url, int episode_id = 0, string title = null)
{
var init = await loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
OnLog($"AnimeON Play: url={url}, episode_id={episode_id}");
string streamLink = null;
if (episode_id > 0)
{
streamLink = await invoke.ResolveEpisodeStream(episode_id);
}
else if (!string.IsNullOrEmpty(url))
{
streamLink = await invoke.ResolveVideoUrl(url);
}
else
{
OnLog("AnimeON Play: empty url");
return OnError("animeon", proxyManager);
}
if (string.IsNullOrEmpty(streamLink))
{
OnLog("AnimeON Play: cannot extract stream");
return OnError("animeon", proxyManager);
}
List<HeadersModel> streamHeaders = null;
bool forceProxy = false;
if (streamLink.Contains("ashdi.vip", StringComparison.OrdinalIgnoreCase))
{
streamHeaders = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", "https://ashdi.vip/")
};
forceProxy = true;
}
string streamUrl = HostStreamProxy(init, accsArgs(streamLink), headers: streamHeaders, force_streamproxy: forceProxy);
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? string.Empty}\"}}";
OnLog("AnimeON Play: return call JSON");
return Content(jsonResult, "application/json; charset=utf-8");
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
}
}

View File

@ -1,43 +0,0 @@
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
namespace AnimeON
{
public class ModInit
{
public static OnlinesSettings AnimeON;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false, useproxy: false)
{
displayname = "🇯🇵 AnimeON",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
AnimeON = ModuleInvoke.Conf("AnimeON", AnimeON).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("animeon");
}
}
}

View File

@ -1,58 +0,0 @@
using System.Collections.Generic;
namespace AnimeON.Models
{
/// <summary> Aggregated structure for AnimeON serial content to match Lampac standard navigation.</summary>
public class AnimeONAggregatedStructure
{
/// <summary>Anime identifier from AnimeON API.</summary>
public int AnimeId { get; set; }
/// <summary>Season number.</summary>
public int Season { get; set; }
/// <summary>Voices mapped by display key e.g. "[Moon] AniUA".</summary>
public Dictionary<string, AnimeONVoiceInfo> Voices { get; set; } = new Dictionary<string, AnimeONVoiceInfo>();
}
/// <summary>Voice information for a specific player/studio combination within a season.</summary>
public class AnimeONVoiceInfo
{
/// <summary>Studio/voice name (e.g., AniUA).</summary>
public string Name { get; set; }
/// <summary>Player type ("moon" or "ashdi").</summary>
public string PlayerType { get; set; }
/// <summary>Display name (e.g., "[Moon] AniUA").</summary>
public string DisplayName { get; set; }
/// <summary>Player identifier from API.</summary>
public int PlayerId { get; set; }
/// <summary>Fundub identifier from API.</summary>
public int FundubId { get; set; }
/// <summary>Flat list of episodes for the selected season.</summary>
public List<AnimeONEpisodeInfo> Episodes { get; set; } = new List<AnimeONEpisodeInfo>();
}
/// <summary>Episode information within a voice.</summary>
public class AnimeONEpisodeInfo
{
/// <summary>Episode number.</summary>
public int Number { get; set; }
/// <summary>Episode title.</summary>
public string Title { get; set; }
/// <summary>Primary HLS link if available.</summary>
public string Hls { get; set; }
/// <summary>Fallback video URL (iframe or direct).</summary>
public string VideoUrl { get; set; }
/// <summary>Episode identifier from API.</summary>
public int EpisodeId { get; set; }
}
}

View File

@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AnimeON.Models
{
public class EmbedModel
{
[JsonPropertyName("translation")]
public string Translation { get; set; }
[JsonPropertyName("links")]
public List<(string link, string quality)> Links { get; set; }
[JsonPropertyName("subtitles")]
public Shared.Models.Templates.SubtitleTpl? Subtitles { get; set; }
[JsonPropertyName("season")]
public int Season { get; set; }
[JsonPropertyName("episode")]
public int Episode { get; set; }
}
}

View File

@ -1,169 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AnimeON.Models
{
public class SearchResponseModel
{
[JsonPropertyName("result")]
public List<SearchModel> Result { get; set; }
[JsonPropertyName("count")]
public int Count { get; set; }
}
public class SearchModel
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("titleUa")]
public string TitleUa { get; set; }
[JsonPropertyName("titleEn")]
public string TitleEn { get; set; }
[JsonPropertyName("releaseDate")]
public string Year { get; set; }
[JsonPropertyName("imdbId")]
public string ImdbId { get; set; }
[JsonPropertyName("season")]
public int Season { get; set; }
}
public class FundubsResponseModel
{
[JsonPropertyName("translations")]
public List<TranslationModel> Translations { get; set; }
}
public class TranslationModel
{
[JsonPropertyName("translation")]
public Fundub Translation { get; set; }
[JsonPropertyName("player")]
public List<Player> Player { get; set; }
}
public class Fundub
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("isSub")]
public bool IsSub { get; set; }
[JsonPropertyName("synonyms")]
public List<string> Synonyms { get; set; }
[JsonPropertyName("studios")]
public List<Studio> Studios { get; set; }
}
public class Studio
{
[JsonPropertyName("slug")]
public string Slug { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("team")]
public object Team { get; set; }
[JsonPropertyName("telegram")]
public string Telegram { get; set; }
[JsonPropertyName("youtube")]
public string Youtube { get; set; }
[JsonPropertyName("patreon")]
public string Patreon { get; set; }
[JsonPropertyName("buymeacoffee")]
public string BuyMeACoffee { get; set; }
[JsonPropertyName("avatar")]
public Avatar Avatar { get; set; }
}
public class Avatar
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("original")]
public string Original { get; set; }
[JsonPropertyName("preview")]
public string Preview { get; set; }
}
public class Player
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("episodesCount")]
public int EpisodesCount { get; set; }
}
public class FundubModel
{
public Fundub Fundub { get; set; }
public List<Player> Player { get; set; }
}
public class EpisodeModel
{
[JsonPropertyName("episodes")]
public List<Episode> Episodes { get; set; }
[JsonPropertyName("anotherPlayer")]
public System.Text.Json.JsonElement AnotherPlayer { get; set; }
}
public class Episode
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("episode")]
public int EpisodeNum { get; set; }
[JsonPropertyName("fileUrl")]
public string Hls { get; set; }
[JsonPropertyName("videoUrl")]
public string VideoUrl { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
public class Movie
{
public string translation { get; set; }
public List<(string link, string quality)> links { get; set; }
public Shared.Models.Templates.SubtitleTpl? subtitles { get; set; }
public int season { get; set; }
public int episode { get; set; }
}
public class Result
{
public List<Movie> movie { get; set; }
}
}

View File

@ -1,29 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AnimeON.Models
{
public class Serial
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("title_ua")]
public string TitleUa { get; set; }
[JsonPropertyName("title_en")]
public string TitleEn { get; set; }
[JsonPropertyName("year")]
public string Year { get; set; }
[JsonPropertyName("imdb_id")]
public string ImdbId { get; set; }
[JsonPropertyName("season")]
public int Season { get; set; }
[JsonPropertyName("voices")]
public List<Voice> Voices { get; set; }
}
}

View File

@ -1,26 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AnimeON.Models
{
public class Voice
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("players")]
public List<VoicePlayer> Players { get; set; }
}
public class VoicePlayer
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}
}

View File

@ -1,35 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace AnimeON
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.AnimeON;
// Визначаємо isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// AnimeON — аніме-провайдер. Додаємо його:
// - при загальному пошуку (serial == -1), або
// - якщо контент визначений як аніме (isanime), або
// - якщо мова невідома (відсутній original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/animeon";
online.Add((init.displayname, url, "animeon", init.displayindex));
}
return online;
}
}
}

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "AnimeON.ModInit",
"online": "AnimeON.OnlineApi"
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,355 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Bamboo.Models;
using HtmlAgilityPack;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
namespace Bamboo
{
public class BambooInvoke
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private readonly OnlinesSettings _init;
private readonly HybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
public BambooInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<SearchResult>> Search(string title, string original_title)
{
string query = !string.IsNullOrEmpty(title) ? title : original_title;
if (string.IsNullOrEmpty(query))
return null;
string memKey = $"Bamboo:search:{query}";
if (_hybridCache.TryGetValue(memKey, out List<SearchResult> cached))
return cached;
try
{
string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={HttpUtility.UrlEncode(query)}";
if (IsNotAllowedHost(searchUrl))
return null;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
_onLog?.Invoke($"Bamboo search: {searchUrl}");
string html = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var results = new List<SearchResult>();
var nodes = doc.DocumentNode.SelectNodes("//li[contains(@class,'slide-item')]");
if (nodes != null)
{
foreach (var node in nodes)
{
string itemTitle = CleanText(node.SelectSingleNode(".//h6")?.InnerText);
string href = ExtractHref(node);
string poster = ExtractPoster(node);
if (string.IsNullOrEmpty(itemTitle) || string.IsNullOrEmpty(href))
continue;
results.Add(new SearchResult
{
Title = itemTitle,
Url = href,
Poster = poster
});
}
}
if (results.Count > 0)
_hybridCache.Set(memKey, results, cacheTime(20, init: _init));
return results;
}
catch (Exception ex)
{
_onLog?.Invoke($"Bamboo search error: {ex.Message}");
return null;
}
}
public async Task<SeriesEpisodes> GetSeriesEpisodes(string href)
{
if (string.IsNullOrEmpty(href))
return null;
string memKey = $"Bamboo:series:{href}";
if (_hybridCache.TryGetValue(memKey, out SeriesEpisodes cached))
return cached;
try
{
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
if (IsNotAllowedHost(href))
return null;
_onLog?.Invoke($"Bamboo series page: {href}");
string html = await Http.Get(href, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var result = new SeriesEpisodes();
bool foundBlocks = false;
var blocks = doc.DocumentNode.SelectNodes("//div[contains(@class,'mt-4')]");
if (blocks != null)
{
foreach (var block in blocks)
{
string header = CleanText(block.SelectSingleNode(".//h3[contains(@class,'my-4')]")?.InnerText);
var episodes = ParseEpisodeSpans(block);
if (episodes.Count == 0)
continue;
foundBlocks = true;
if (!string.IsNullOrEmpty(header) && header.Contains("Субтитри", StringComparison.OrdinalIgnoreCase))
{
result.Sub.AddRange(episodes);
}
else if (!string.IsNullOrEmpty(header) && header.Contains("Озвучення", StringComparison.OrdinalIgnoreCase))
{
result.Dub.AddRange(episodes);
}
}
}
if (!foundBlocks || (result.Sub.Count == 0 && result.Dub.Count == 0))
{
var fallback = ParseEpisodeSpans(doc.DocumentNode);
if (fallback.Count > 0)
result.Dub.AddRange(fallback);
}
if (result.Sub.Count == 0)
{
var fallback = ParseEpisodeSpans(doc.DocumentNode);
if (fallback.Count > 0)
result.Sub.AddRange(fallback);
}
_hybridCache.Set(memKey, result, cacheTime(30, init: _init));
return result;
}
catch (Exception ex)
{
_onLog?.Invoke($"Bamboo series error: {ex.Message}");
return null;
}
}
public async Task<List<StreamInfo>> GetMovieStreams(string href)
{
if (string.IsNullOrEmpty(href))
return null;
string memKey = $"Bamboo:movie:{href}";
if (_hybridCache.TryGetValue(memKey, out List<StreamInfo> cached))
return cached;
try
{
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
if (IsNotAllowedHost(href))
return null;
_onLog?.Invoke($"Bamboo movie page: {href}");
string html = await Http.Get(href, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html))
return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var streams = new List<StreamInfo>();
var nodes = doc.DocumentNode.SelectNodes("//span[contains(@class,'mr-3') and @data-file]");
if (nodes != null)
{
foreach (var node in nodes)
{
string dataFile = node.GetAttributeValue("data-file", "");
if (string.IsNullOrEmpty(dataFile))
continue;
string title = node.GetAttributeValue("data-title", "");
title = string.IsNullOrEmpty(title) ? CleanText(node.InnerText) : title;
streams.Add(new StreamInfo
{
Title = title,
Url = NormalizeUrl(dataFile)
});
}
}
if (streams.Count > 0)
_hybridCache.Set(memKey, streams, cacheTime(30, init: _init));
return streams;
}
catch (Exception ex)
{
_onLog?.Invoke($"Bamboo movie error: {ex.Message}");
return null;
}
}
private List<EpisodeInfo> ParseEpisodeSpans(HtmlNode scope)
{
var episodes = new List<EpisodeInfo>();
var nodes = scope.SelectNodes(".//span[@data-file]");
if (nodes == null)
return episodes;
foreach (var node in nodes)
{
string dataFile = node.GetAttributeValue("data-file", "");
if (string.IsNullOrEmpty(dataFile))
continue;
string title = node.GetAttributeValue("data-title", "");
if (string.IsNullOrEmpty(title))
title = CleanText(node.InnerText);
int? episodeNum = ExtractEpisodeNumber(title);
episodes.Add(new EpisodeInfo
{
Title = string.IsNullOrEmpty(title) ? "Episode" : title,
Url = NormalizeUrl(dataFile),
Episode = episodeNum
});
}
return episodes;
}
private string ExtractHref(HtmlNode node)
{
var link = node.SelectSingleNode(".//a[contains(@class,'hover-buttons')]")
?? node.SelectSingleNode(".//a[@href]");
if (link == null)
return string.Empty;
string href = link.GetAttributeValue("href", "");
return NormalizeUrl(href);
}
private string ExtractPoster(HtmlNode node)
{
var img = node.SelectSingleNode(".//img");
if (img == null)
return string.Empty;
string src = img.GetAttributeValue("src", "");
if (string.IsNullOrEmpty(src))
src = img.GetAttributeValue("data-src", "");
return NormalizeUrl(src);
}
private string NormalizeUrl(string url)
{
if (string.IsNullOrEmpty(url))
return string.Empty;
if (url.StartsWith("//"))
return IsNotAllowedHost($"https:{url}") ? string.Empty : $"https:{url}";
if (url.StartsWith("/"))
return IsNotAllowedHost(_init.host) ? string.Empty : $"{_init.host}{url}";
return IsNotAllowedHost(url) ? string.Empty : url;
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
private static int? ExtractEpisodeNumber(string title)
{
if (string.IsNullOrEmpty(title))
return null;
var match = Regex.Match(title, @"(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int value))
return value;
return null;
}
private static string CleanText(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return HtmlEntity.DeEntitize(value).Trim();
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
}
}

View File

@ -1,119 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Bamboo.Models;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
namespace Bamboo.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.Bamboo);
}
[HttpGet]
[Route("bamboo")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, bool rjson = false, string href = null)
{
var init = await loadKit(ModInit.Bamboo);
if (!init.enable)
return Forbid();
var invoke = new BambooInvoke(init, hybridCache, OnLog, proxyManager);
string itemUrl = href;
if (string.IsNullOrEmpty(itemUrl))
{
var searchResults = await invoke.Search(title, original_title);
if (searchResults == null || searchResults.Count == 0)
return OnError("bamboo", proxyManager);
if (searchResults.Count > 1)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string link = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, res.Poster);
}
return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
}
itemUrl = searchResults[0].Url;
}
if (serial == 1)
{
var series = await invoke.GetSeriesEpisodes(itemUrl);
if (series == null || (series.Sub.Count == 0 && series.Dub.Count == 0))
return OnError("bamboo", proxyManager);
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
var availableVoices = new List<(string key, string name, List<EpisodeInfo> episodes)>();
if (series.Sub.Count > 0)
availableVoices.Add(("sub", "Субтитри", series.Sub));
if (series.Dub.Count > 0)
availableVoices.Add(("dub", "Озвучення", series.Dub));
if (string.IsNullOrEmpty(t))
t = availableVoices.First().key;
foreach (var voice in availableVoices)
{
string voiceLink = $"{host}/bamboo?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={voice.key}&href={HttpUtility.UrlEncode(itemUrl)}";
voice_tpl.Append(voice.name, voice.key == t, voiceLink);
}
var selected = availableVoices.FirstOrDefault(v => v.key == t);
if (selected.episodes == null || selected.episodes.Count == 0)
return OnError("bamboo", proxyManager);
int index = 1;
foreach (var ep in selected.episodes.OrderBy(e => e.Episode ?? int.MaxValue))
{
int episodeNumber = ep.Episode ?? index;
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title;
string streamUrl = HostStreamProxy(init, accsArgs(ep.Url));
episode_tpl.Append(episodeName, title ?? original_title, "1", episodeNumber.ToString("D2"), streamUrl);
index++;
}
if (rjson)
return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8");
return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
else
{
var streams = await invoke.GetMovieStreams(itemUrl);
if (streams == null || streams.Count == 0)
return OnError("bamboo", proxyManager);
var movie_tpl = new MovieTpl(title, original_title);
for (int i = 0; i < streams.Count; i++)
{
var stream = streams[i];
string label = !string.IsNullOrEmpty(stream.Title) ? stream.Title : $"Варіант {i + 1}";
string streamUrl = HostStreamProxy(init, accsArgs(stream.Url));
movie_tpl.Append(label, streamUrl);
}
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
}
}

View File

@ -1,35 +0,0 @@
using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace Bamboo
{
public class ModInit
{
public static OnlinesSettings Bamboo;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
Bamboo = new OnlinesSettings("Bamboo", "https://bambooua.com", streamproxy: false, useproxy: false)
{
displayname = "BambooUA",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
Bamboo = ModuleInvoke.Conf("Bamboo", Bamboo).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("bamboo");
}
}
}

View File

@ -1,30 +0,0 @@
using System.Collections.Generic;
namespace Bamboo.Models
{
public class SearchResult
{
public string Title { get; set; }
public string Url { get; set; }
public string Poster { get; set; }
}
public class EpisodeInfo
{
public string Title { get; set; }
public string Url { get; set; }
public int? Episode { get; set; }
}
public class StreamInfo
{
public string Title { get; set; }
public string Url { get; set; }
}
public class SeriesEpisodes
{
public List<EpisodeInfo> Sub { get; set; } = new();
public List<EpisodeInfo> Dub { get; set; } = new();
}
}

View File

@ -1,25 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace Bamboo
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.Bamboo;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/bamboo";
online.Add((init.displayname, url, "bamboo", init.displayindex));
}
return online;
}
}
}

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "Bamboo.ModInit",
"online": "Bamboo.OnlineApi"
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,396 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models;
using System.Text.RegularExpressions;
using System.Text;
using HtmlAgilityPack;
using CikavaIdeya.Models;
using Shared.Engine;
using System.Linq;
namespace CikavaIdeya
{
public class CikavaIdeyaInvoke
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private OnlinesSettings _init;
private HybridCache _hybridCache;
private Action<string> _onLog;
private ProxyManager _proxyManager;
public CikavaIdeyaInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<CikavaIdeya.Models.EpisodeLinkInfo>> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false)
{
string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title;
string memKey = $"CikavaIdeya:search:{filmTitle}:{year}:{isfilm}";
if (_hybridCache.TryGetValue(memKey, out List<CikavaIdeya.Models.EpisodeLinkInfo> res))
return res;
try
{
// Спочатку шукаємо по title
res = await PerformSearch(title, year);
// Якщо нічого не знайдено і є original_title, шукаємо по ньому
if ((res == null || res.Count == 0) && !string.IsNullOrEmpty(original_title) && original_title != title)
{
_onLog($"No results for '{title}', trying search by original title '{original_title}'");
res = await PerformSearch(original_title, year);
// Оновлюємо ключ кешу для original_title
if (res != null && res.Count > 0)
{
memKey = $"CikavaIdeya:search:{original_title}:{year}:{isfilm}";
}
}
if (res != null && res.Count > 0)
{
_hybridCache.Set(memKey, res, cacheTime(20));
return res;
}
}
catch (Exception ex)
{
_onLog($"CikavaIdeya search error: {ex.Message}");
}
return null;
}
async Task<List<EpisodeLinkInfo>> PerformSearch(string searchTitle, int year)
{
try
{
string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={System.Web.HttpUtility.UrlEncode(searchTitle)}";
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) };
if (IsNotAllowedHost(searchUrl))
return null;
var searchHtml = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get());
// Перевіряємо, чи є результати пошуку
if (searchHtml.Contains("На жаль, пошук на сайті не дав жодних результатів"))
{
_onLog($"No search results for '{searchTitle}'");
return new List<CikavaIdeya.Models.EpisodeLinkInfo>();
}
var doc = new HtmlDocument();
doc.LoadHtml(searchHtml);
var filmNodes = doc.DocumentNode.SelectNodes("//div[@class='th-item']");
if (filmNodes == null)
{
_onLog($"No film nodes found for '{searchTitle}'");
return new List<CikavaIdeya.Models.EpisodeLinkInfo>();
}
string filmUrl = null;
foreach (var filmNode in filmNodes)
{
var titleNode = filmNode.SelectSingleNode(".//div[@class='th-title']");
if (titleNode == null || !titleNode.InnerText.Trim().ToLower().Contains(searchTitle.ToLower())) continue;
var descNode = filmNode.SelectSingleNode(".//div[@class='th-subtitle']");
if (year > 0 && (descNode?.InnerText ?? "").Contains(year.ToString()))
{
var linkNode = filmNode.SelectSingleNode(".//a[@class='th-in']");
if (linkNode != null)
{
filmUrl = linkNode.GetAttributeValue("href", "");
break;
}
}
}
if (filmUrl == null)
{
var firstNode = filmNodes.FirstOrDefault()?.SelectSingleNode(".//a[@class='th-in']");
if (firstNode != null)
filmUrl = firstNode.GetAttributeValue("href", "");
}
if (filmUrl == null)
{
_onLog($"No film URL found for '{searchTitle}'");
return new List<CikavaIdeya.Models.EpisodeLinkInfo>();
}
if (!filmUrl.StartsWith("http"))
filmUrl = _init.host + filmUrl;
// Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди)
if (IsNotAllowedHost(filmUrl))
return null;
var filmHtml = await Http.Get(filmUrl, headers: headers, proxy: _proxyManager.Get());
// Перевіряємо, чи не видалено контент
if (filmHtml.Contains("Видалено на прохання правовласника"))
{
_onLog($"Content removed on copyright holder request: {filmUrl}");
return new List<CikavaIdeya.Models.EpisodeLinkInfo>();
}
doc.LoadHtml(filmHtml);
// Знаходимо JavaScript з даними про епізоди
var scriptNodes = doc.DocumentNode.SelectNodes("//script");
if (scriptNodes != null)
{
foreach (var scriptNode in scriptNodes)
{
var scriptContent = scriptNode.InnerText;
if (scriptContent.Contains("switches = Object"))
{
_onLog($"Found switches script: {scriptContent}");
// Парсимо структуру switches
var match = Regex.Match(scriptContent, @"switches = Object\((\{.*\})\);", RegexOptions.Singleline);
if (match.Success)
{
string switchesJson = match.Groups[1].Value;
_onLog($"Parsed switches JSON: {switchesJson}");
// Спрощений парсинг JSON-подібної структури
var res = ParseSwitchesJson(switchesJson, _init.host, filmUrl);
_onLog($"Parsed episodes count: {res.Count}");
foreach (var ep in res)
{
_onLog($"Episode: season={ep.season}, episode={ep.episode}, title={ep.title}, url={ep.url}");
}
return res;
}
}
}
}
}
catch (Exception ex)
{
_onLog($"PerformSearch error for '{searchTitle}': {ex.Message}");
}
return new List<EpisodeLinkInfo>();
}
List<CikavaIdeya.Models.EpisodeLinkInfo> ParseSwitchesJson(string json, string host, string baseUrl)
{
var result = new List<CikavaIdeya.Models.EpisodeLinkInfo>();
try
{
_onLog($"Parsing switches JSON: {json}");
// Спрощений парсинг JSON-подібної структури
// Приклад для серіалу: {"Player1":{"1 сезон":{"1 серія":"https://ashdi.vip/vod/57364",...},"2 сезон":{"1 серія":"https://ashdi.vip/vod/118170",...}}}
// Приклад для фільму: {"Player1":"https://ashdi.vip/vod/162246"}
// Знаходимо плеєр Player1
// Спочатку спробуємо знайти об'єкт Player1
var playerObjectMatch = Regex.Match(json, @"""Player1""\s*:\s*(\{(?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!)))", RegexOptions.Singleline);
if (playerObjectMatch.Success)
{
string playerContent = playerObjectMatch.Groups[1].Value;
_onLog($"Player1 object content: {playerContent}");
// Це серіал, парсимо сезони
var seasonMatches = Regex.Matches(playerContent, @"""([^""]+?сезон[^""]*?)""\s*:\s*\{((?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!)))\}", RegexOptions.Singleline);
_onLog($"Found {seasonMatches.Count} seasons");
foreach (Match seasonMatch in seasonMatches)
{
string seasonName = seasonMatch.Groups[1].Value;
string seasonContent = seasonMatch.Groups[2].Value;
_onLog($"Season: {seasonName}, Content: {seasonContent}");
// Витягуємо номер сезону
var seasonNumMatch = Regex.Match(seasonName, @"(\d+)");
int seasonNum = seasonNumMatch.Success ? int.Parse(seasonNumMatch.Groups[1].Value) : 1;
_onLog($"Season number: {seasonNum}");
// Парсимо епізоди
var episodeMatches = Regex.Matches(seasonContent, @"""([^""]+?)""\s*:\s*""([^""]+?)""", RegexOptions.Singleline);
_onLog($"Found {episodeMatches.Count} episodes in season {seasonNum}");
foreach (Match episodeMatch in episodeMatches)
{
string episodeName = episodeMatch.Groups[1].Value;
string episodeUrl = episodeMatch.Groups[2].Value;
_onLog($"Episode: {episodeName}, URL: {episodeUrl}");
// Витягуємо номер епізоду
var episodeNumMatch = Regex.Match(episodeName, @"(\d+)");
int episodeNum = episodeNumMatch.Success ? int.Parse(episodeNumMatch.Groups[1].Value) : 1;
result.Add(new CikavaIdeya.Models.EpisodeLinkInfo
{
url = episodeUrl,
title = episodeName,
season = seasonNum,
episode = episodeNum
});
}
}
}
else
{
// Якщо не знайшли об'єкт, спробуємо знайти просте значення
var playerStringMatch = Regex.Match(json, @"""Player1""\s*:\s*(""([^""]+)"")", RegexOptions.Singleline);
if (playerStringMatch.Success)
{
string playerContent = playerStringMatch.Groups[1].Value;
_onLog($"Player1 string content: {playerContent}");
// Якщо це фільм (просте значення)
if (playerContent.StartsWith("\"") && playerContent.EndsWith("\""))
{
string filmUrl = playerContent.Trim('"');
result.Add(new CikavaIdeya.Models.EpisodeLinkInfo
{
url = filmUrl,
title = "Фільм",
season = 1,
episode = 1
});
}
}
else
{
_onLog("Player1 not found");
}
}
}
catch (Exception ex)
{
_onLog($"ParseSwitchesJson error: {ex.Message}");
}
return result;
}
public async Task<CikavaIdeya.Models.PlayResult> ParseEpisode(string url)
{
var result = new CikavaIdeya.Models.PlayResult() { streams = new List<(string, string)>() };
try
{
// Якщо це вже iframe URL (наприклад, з switches), повертаємо його
if (url.Contains("ashdi.vip"))
{
_onLog($"ParseEpisode: URL contains ashdi.vip, calling GetStreamUrlFromAshdi");
string streamUrl = await GetStreamUrlFromAshdi(url);
_onLog($"ParseEpisode: GetStreamUrlFromAshdi returned {streamUrl}");
if (!string.IsNullOrEmpty(streamUrl))
{
result.streams.Add((streamUrl, "hls"));
_onLog($"ParseEpisode: added stream URL to result.streams");
return result;
}
// Якщо не вдалося отримати посилання на поток, повертаємо iframe URL
_onLog($"ParseEpisode: stream URL is null or empty, setting iframe_url");
result.iframe_url = url;
return result;
}
// Інакше парсимо сторінку
if (IsNotAllowedHost(url))
return result;
string html = await Http.Get(url, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", _init.host) }, proxy: _proxyManager.Get());
var doc = new HtmlDocument();
doc.LoadHtml(html);
var iframe = doc.DocumentNode.SelectSingleNode("//div[@class='video-box']//iframe");
if (iframe != null)
{
string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&");
if (iframeUrl.StartsWith("//"))
iframeUrl = "https:" + iframeUrl;
result.iframe_url = iframeUrl;
return result;
}
}
catch (Exception ex)
{
_onLog($"ParseEpisode error: {ex.Message}");
}
return result;
}
public async Task<string> GetStreamUrlFromAshdi(string url)
{
try
{
_onLog($"GetStreamUrlFromAshdi: trying to get stream URL from {url}");
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") };
if (IsNotAllowedHost(url))
return null;
string html = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
_onLog($"GetStreamUrlFromAshdi: received HTML, length={html.Length}");
// Знаходимо JavaScript код з об'єктом player
var match = Regex.Match(html, @"var\s+player\s*=\s*new\s+Playerjs[\s\S]*?\(\s*({[\s\S]*?})\s*\)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
if (match.Success)
{
_onLog($"GetStreamUrlFromAshdi: found player object");
string playerJson = match.Groups[1].Value;
_onLog($"GetStreamUrlFromAshdi: playerJson={playerJson}");
// Знаходимо поле file
var fileMatch = Regex.Match(playerJson, @"file\s*:\s*['""]([^'""]+)['""]", RegexOptions.Multiline | RegexOptions.IgnoreCase);
if (fileMatch.Success)
{
_onLog($"GetStreamUrlFromAshdi: found file URL: {fileMatch.Groups[1].Value}");
return fileMatch.Groups[1].Value;
}
else
{
_onLog($"GetStreamUrlFromAshdi: file URL not found in playerJson");
}
}
else
{
_onLog($"GetStreamUrlFromAshdi: player object not found in HTML");
}
}
catch (Exception ex)
{
_onLog($"GetStreamUrlFromAshdi error: {ex.Message}");
}
return null;
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
}
}

View File

@ -1,417 +0,0 @@
using Shared.Engine;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using HtmlAgilityPack;
using Shared;
using Shared.Models.Templates;
using System.Text.RegularExpressions;
using System.Text;
using Shared.Models.Online.Settings;
using Shared.Models;
using CikavaIdeya.Models;
namespace CikavaIdeya.Controllers
{
public class Controller : BaseOnlineController
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.CikavaIdeya);
}
[HttpGet]
[Route("cikavaideya")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int e = -1, bool play = false, bool rjson = false)
{
var init = await loadKit(ModInit.CikavaIdeya);
if (!init.enable)
return Forbid();
var invoke = new CikavaIdeyaInvoke(init, hybridCache, OnLog, proxyManager);
var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0);
if (episodesInfo == null)
return Content("CikavaIdeya", "text/html; charset=utf-8");
if (play)
{
var episode = episodesInfo.FirstOrDefault(ep => ep.season == s && ep.episode == e);
if (serial == 0) // для фильма берем первый
episode = episodesInfo.FirstOrDefault();
if (episode == null)
return Content("CikavaIdeya", "text/html; charset=utf-8");
OnLog($"Controller: calling invoke.ParseEpisode with URL: {episode.url}");
var playResult = await invoke.ParseEpisode(episode.url);
OnLog($"Controller: invoke.ParseEpisode returned playResult with streams.Count={playResult.streams?.Count ?? 0}, iframe_url={playResult.iframe_url}");
if (playResult.streams != null && playResult.streams.Count > 0)
{
string streamLink = playResult.streams.First().link;
string streamUrl = HostStreamProxy(init, accsArgs(streamLink));
OnLog($"Controller: redirecting to stream URL: {streamUrl}");
return Redirect(streamUrl);
}
if (!string.IsNullOrEmpty(playResult.iframe_url))
{
OnLog($"Controller: redirecting to iframe URL: {playResult.iframe_url}");
// Для CikavaIdeya ми просто повертаємо iframe URL
return Redirect(playResult.iframe_url);
}
if (playResult.streams != null && playResult.streams.Count > 0)
return Redirect(HostStreamProxy(init, accsArgs(playResult.streams.First().link)));
return Content("CikavaIdeya", "text/html; charset=utf-8");
}
if (serial == 1)
{
if (s == -1) // Выбор сезона
{
var seasons = episodesInfo.GroupBy(ep => ep.season).ToDictionary(k => k.Key, v => v.ToList());
OnLog($"Grouped seasons count: {seasons.Count}");
foreach (var season in seasons)
{
OnLog($"Season {season.Key}: {season.Value.Count} episodes");
}
var season_tpl = new SeasonTpl(seasons.Count);
foreach (var season in seasons.OrderBy(i => i.Key))
{
string link = $"{host}/cikavaideya?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={season.Key}";
season_tpl.Append($"Сезон {season.Key}", link, $"{season.Key}");
}
OnLog("Before generating season template HTML");
string htmlContent = season_tpl.ToHtml();
OnLog($"Season template HTML: {htmlContent}");
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(htmlContent, "text/html; charset=utf-8");
}
// Выбор эпизода
var episodes = episodesInfo.Where(ep => ep.season == s).OrderBy(ep => ep.episode).ToList();
var movie_tpl = new MovieTpl(title, original_title, episodes.Count);
foreach(var ep in episodes)
{
string link = $"{host}/cikavaideya?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&e={ep.episode}&play=true";
movie_tpl.Append(ep.title, accsArgs(link), method: "play");
}
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
else // Фильм
{
string link = $"{host}/cikavaideya?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&play=true";
var tpl = new MovieTpl(title, original_title, 1);
tpl.Append(title, accsArgs(link), method: "play");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
}
}
async ValueTask<List<EpisodeLinkInfo>> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false)
{
string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title;
string memKey = $"CikavaIdeya:search:{filmTitle}:{year}:{isfilm}";
if (hybridCache.TryGetValue(memKey, out List<EpisodeLinkInfo> res))
return res;
try
{
// Спочатку шукаємо по title
res = await PerformSearch(init, title, year);
// Якщо нічого не знайдено і є original_title, шукаємо по ньому
if ((res == null || res.Count == 0) && !string.IsNullOrEmpty(original_title) && original_title != title)
{
OnLog($"No results for '{title}', trying search by original title '{original_title}'");
res = await PerformSearch(init, original_title, year);
// Оновлюємо ключ кешу для original_title
if (res != null && res.Count > 0)
{
memKey = $"CikavaIdeya:search:{original_title}:{year}:{isfilm}";
}
}
if (res != null && res.Count > 0)
{
hybridCache.Set(memKey, res, cacheTime(20));
return res;
}
}
catch (Exception ex)
{
OnLog($"CikavaIdeya search error: {ex.Message}");
}
return null;
}
async Task<List<EpisodeLinkInfo>> PerformSearch(OnlinesSettings init, string searchTitle, int year)
{
try
{
string searchUrl = $"{init.host}/index.php?do=search&subaction=search&story={HttpUtility.UrlEncode(searchTitle)}";
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) };
if (IsNotAllowedHost(searchUrl))
return null;
var searchHtml = await Http.Get(searchUrl, headers: headers);
// Перевіряємо, чи є результати пошуку
if (searchHtml.Contains("На жаль, пошук на сайті не дав жодних результатів"))
{
OnLog($"No search results for '{searchTitle}'");
return new List<EpisodeLinkInfo>();
}
var doc = new HtmlDocument();
doc.LoadHtml(searchHtml);
var filmNodes = doc.DocumentNode.SelectNodes("//div[@class='th-item']");
if (filmNodes == null)
{
OnLog($"No film nodes found for '{searchTitle}'");
return new List<EpisodeLinkInfo>();
}
string filmUrl = null;
foreach (var filmNode in filmNodes)
{
var titleNode = filmNode.SelectSingleNode(".//div[@class='th-title']");
if (titleNode == null || !titleNode.InnerText.Trim().ToLower().Contains(searchTitle.ToLower())) continue;
var descNode = filmNode.SelectSingleNode(".//div[@class='th-subtitle']");
if (year > 0 && (descNode?.InnerText ?? "").Contains(year.ToString()))
{
var linkNode = filmNode.SelectSingleNode(".//a[@class='th-in']");
if (linkNode != null)
{
filmUrl = linkNode.GetAttributeValue("href", "");
break;
}
}
}
if (filmUrl == null)
{
var firstNode = filmNodes.FirstOrDefault()?.SelectSingleNode(".//a[@class='th-in']");
if (firstNode != null)
filmUrl = firstNode.GetAttributeValue("href", "");
}
if (filmUrl == null)
{
OnLog($"No film URL found for '{searchTitle}'");
return new List<EpisodeLinkInfo>();
}
if (!filmUrl.StartsWith("http"))
filmUrl = init.host + filmUrl;
// Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди)
if (IsNotAllowedHost(filmUrl))
return null;
var filmHtml = await Http.Get(filmUrl, headers: headers);
// Перевіряємо, чи не видалено контент
if (filmHtml.Contains("Видалено на прохання правовласника"))
{
OnLog($"Content removed on copyright holder request: {filmUrl}");
return new List<EpisodeLinkInfo>();
}
doc.LoadHtml(filmHtml);
// Знаходимо JavaScript з даними про епізоди
var scriptNodes = doc.DocumentNode.SelectNodes("//script");
if (scriptNodes != null)
{
foreach (var scriptNode in scriptNodes)
{
var scriptContent = scriptNode.InnerText;
if (scriptContent.Contains("switches = Object"))
{
OnLog($"Found switches script: {scriptContent}");
// Парсимо структуру switches
var match = Regex.Match(scriptContent, @"switches = Object\((\{.*\})\);", RegexOptions.Singleline);
if (match.Success)
{
string switchesJson = match.Groups[1].Value;
OnLog($"Parsed switches JSON: {switchesJson}");
// Спрощений парсинг JSON-подібної структури
var res = ParseSwitchesJson(switchesJson, init.host, filmUrl);
OnLog($"Parsed episodes count: {res.Count}");
foreach (var ep in res)
{
OnLog($"Episode: season={ep.season}, episode={ep.episode}, title={ep.title}, url={ep.url}");
}
return res;
}
}
}
}
}
catch (Exception ex)
{
OnLog($"PerformSearch error for '{searchTitle}': {ex.Message}");
}
return new List<EpisodeLinkInfo>();
}
List<EpisodeLinkInfo> ParseSwitchesJson(string json, string host, string baseUrl)
{
var result = new List<EpisodeLinkInfo>();
try
{
OnLog($"Parsing switches JSON: {json}");
// Спрощений парсинг JSON-подібної структури
// Приклад для серіалу: {"Player1":{"1 сезон":{"1 серія":"https://ashdi.vip/vod/57364",...},"2 сезон":{"1 серія":"https://ashdi.vip/vod/118170",...}}}
// Приклад для фільму: {"Player1":"https://ashdi.vip/vod/162246"}
// Знаходимо плеєр Player1
// Спочатку спробуємо знайти об'єкт Player1
var playerObjectMatch = Regex.Match(json, @"""Player1""\s*:\s*(\{(?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!)))", RegexOptions.Singleline);
if (playerObjectMatch.Success)
{
string playerContent = playerObjectMatch.Groups[1].Value;
OnLog($"Player1 object content: {playerContent}");
// Це серіал, парсимо сезони
var seasonMatches = Regex.Matches(playerContent, @"""([^""]+?сезон[^""]*?)""\s*:\s*\{((?:[^{}]|(?<open>\{)|(?<-open>\}))+(?(open)(?!)))\}", RegexOptions.Singleline);
OnLog($"Found {seasonMatches.Count} seasons");
foreach (Match seasonMatch in seasonMatches)
{
string seasonName = seasonMatch.Groups[1].Value;
string seasonContent = seasonMatch.Groups[2].Value;
OnLog($"Season: {seasonName}, Content: {seasonContent}");
// Витягуємо номер сезону
var seasonNumMatch = Regex.Match(seasonName, @"(\d+)");
int seasonNum = seasonNumMatch.Success ? int.Parse(seasonNumMatch.Groups[1].Value) : 1;
OnLog($"Season number: {seasonNum}");
// Парсимо епізоди
var episodeMatches = Regex.Matches(seasonContent, @"""([^""]+?)""\s*:\s*""([^""]+?)""", RegexOptions.Singleline);
OnLog($"Found {episodeMatches.Count} episodes in season {seasonNum}");
foreach (Match episodeMatch in episodeMatches)
{
string episodeName = episodeMatch.Groups[1].Value;
string episodeUrl = episodeMatch.Groups[2].Value;
OnLog($"Episode: {episodeName}, URL: {episodeUrl}");
// Витягуємо номер епізоду
var episodeNumMatch = Regex.Match(episodeName, @"(\d+)");
int episodeNum = episodeNumMatch.Success ? int.Parse(episodeNumMatch.Groups[1].Value) : 1;
result.Add(new EpisodeLinkInfo
{
url = episodeUrl,
title = episodeName,
season = seasonNum,
episode = episodeNum
});
}
}
}
else
{
// Якщо не знайшли об'єкт, спробуємо знайти просте значення
var playerStringMatch = Regex.Match(json, @"""Player1""\s*:\s*(""([^""]+)"")", RegexOptions.Singleline);
if (playerStringMatch.Success)
{
string playerContent = playerStringMatch.Groups[1].Value;
OnLog($"Player1 string content: {playerContent}");
// Якщо це фільм (просте значення)
if (playerContent.StartsWith("\"") && playerContent.EndsWith("\""))
{
string filmUrl = playerContent.Trim('"');
result.Add(new EpisodeLinkInfo
{
url = filmUrl,
title = "Фільм",
season = 1,
episode = 1
});
}
}
else
{
OnLog("Player1 not found");
}
}
}
catch (Exception ex)
{
OnLog($"ParseSwitchesJson error: {ex.Message}");
}
return result;
}
async Task<PlayResult> ParseEpisode(OnlinesSettings init, string url)
{
var result = new PlayResult() { streams = new List<(string, string)>() };
try
{
// Якщо це вже iframe URL (наприклад, з switches), повертаємо його
if (url.Contains("ashdi.vip"))
{
result.iframe_url = url;
return result;
}
// Інакше парсимо сторінку
if (IsNotAllowedHost(url))
return result;
string html = await Http.Get(url, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
var doc = new HtmlDocument();
doc.LoadHtml(html);
var iframe = doc.DocumentNode.SelectSingleNode("//div[@class='video-box']//iframe");
if (iframe != null)
{
string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&");
if (iframeUrl.StartsWith("//"))
iframeUrl = "https:" + iframeUrl;
result.iframe_url = iframeUrl;
return result;
}
}
catch (Exception ex)
{
OnLog($"ParseEpisode error: {ex.Message}");
}
return result;
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
}
}

View File

@ -1,43 +0,0 @@
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
namespace CikavaIdeya
{
public class ModInit
{
public static OnlinesSettings CikavaIdeya;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
CikavaIdeya = new OnlinesSettings("CikavaIdeya", "https://cikava-ideya.top", streamproxy: false, useproxy: false)
{
displayname = "ЦікаваІдея",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "a",
password = "a",
list = new string[] { "socks5://IP:PORT" }
}
};
CikavaIdeya = ModuleInvoke.Conf("CikavaIdeya", CikavaIdeya).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("cikavaideya");
}
}
}

View File

@ -1,16 +0,0 @@
using System.Text.Json.Serialization;
namespace CikavaIdeya.Models
{
public class EpisodeModel
{
[JsonPropertyName("episode_number")]
public int EpisodeNumber { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("url")]
public string Url { get; set; }
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
namespace CikavaIdeya.Models
{
public class EpisodeLinkInfo
{
public string url { get; set; }
public string title { get; set; }
public int season { get; set; }
public int episode { get; set; }
}
public class PlayResult
{
public string iframe_url { get; set; }
public List<(string link, string quality)> streams { get; set; }
}
}

View File

@ -1,17 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CikavaIdeya.Models
{
public class CikavaIdeyaPlayerModel
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("qualities")]
public List<(string link, string quality)> Qualities { get; set; }
[JsonPropertyName("subtitles")]
public Shared.Models.Templates.SubtitleTpl? Subtitles { get; set; }
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CikavaIdeya.Models
{
public class SeasonModel
{
[JsonPropertyName("season_number")]
public int SeasonNumber { get; set; }
[JsonPropertyName("episodes")]
public List<EpisodeModel> Episodes { get; set; }
}
}

View File

@ -1,34 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace CikavaIdeya
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.CikavaIdeya;
// Визначення isAnime згідно Lampac (Deepwiki): original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// CikavaIdeya — не-аніме провайдер. Додаємо якщо:
// - загальний пошук (serial == -1), або
// - контент НЕ аніме (!isanime), або
// - мова невідома (немає original_language)
if (init.enable && !init.rip && (serial == -1 || !isanime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/cikavaideya";
online.Add((init.displayname, url, "cikavaideya", init.displayindex));
}
return online;
}
}
}

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "CikavaIdeya.ModInit",
"online": "CikavaIdeya.OnlineApi"
}

201
LICENSE
View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,76 +0,0 @@
# Ukraine online source for Lampac
- [x] AnimeON
- [x] [RIP] AniHUB
- [x] BambooUA
- [x] CikavaIdeya
- [x] StarLight
- [x] UAKino
- [x] UAFlix
- [x] UATuTFun
- [x] Unimay
## Installation
1. Clone the repository:
```bash
git clone https://github.com/lampac-ukraine/lampac-ukraine.git .
```
2. Move the modules to the correct directory:
- If Lampac is installed system-wide, move the modules to the `module` directory.
- If Lampac is running in Docker, mount the volume:
```bash
-v /path/to/your/cloned/repo/Uaflix:/home/module/Uaflix
```
## Auto installation
If Lampac version 148.1 and newer
Create or update the module/repository.yaml file
```YAML
- repository: https://github.com/lampame/lampac-ukraine
branch: main
modules:
- AnimeON
- Anihub
- Unimay
- CikavaIdeya
- Uaflix
- UaTUT
- Bamboo
- UAKino
- StarLight
```
branch - optional, default main
modules - optional, if not specified, all modules from the repository will be installed
## Init support
```json
"Uaflix": {
"enable": true,
"domain": "https://uaflix.net",
"displayname": "Uaflix",
"streamproxy": false,
"useproxy": false,
"proxy": {
"useAuth": true,
"username": "FooBAR",
"password": "Strong_password",
"list": [
"socks5://adress:port"
]
},
"displayindex": 1
}
```
## Donate
Support the author: https://lampame.donatik.me

View File

@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
using StarLight.Models;
namespace StarLight.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.StarLight);
}
[HttpGet]
[Route("starlight")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, int s = -1, bool rjson = false, string href = null)
{
var init = await loadKit(ModInit.StarLight);
if (!init.enable)
return Forbid();
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
string itemUrl = href;
if (string.IsNullOrEmpty(itemUrl))
{
var searchResults = await invoke.Search(title, original_title);
if (searchResults == null || searchResults.Count == 0)
return OnError("starlight", proxyManager);
if (searchResults.Count > 1)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Href)}";
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, string.Empty);
}
return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
}
itemUrl = searchResults[0].Href;
}
var project = await invoke.GetProject(itemUrl);
if (project == null)
return OnError("starlight", proxyManager);
if (serial == 1 && project.Seasons.Count > 0)
{
if (s == -1)
{
var season_tpl = new SeasonTpl(project.Seasons.Count);
for (int i = 0; i < project.Seasons.Count; i++)
{
var seasonInfo = project.Seasons[i];
string seasonName = string.IsNullOrEmpty(seasonInfo.Title) ? $"Сезон {i + 1}" : seasonInfo.Title;
string link = $"{host}/starlight?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}&href={HttpUtility.UrlEncode(itemUrl)}";
season_tpl.Append(seasonName, link, i.ToString());
}
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
if (s < 0 || s >= project.Seasons.Count)
return OnError("starlight", proxyManager);
var season = project.Seasons[s];
string seasonSlug = season.Slug;
var episodes = invoke.GetEpisodes(project, seasonSlug);
if (episodes == null || episodes.Count == 0)
return OnError("starlight", proxyManager);
var episode_tpl = new EpisodeTpl();
int index = 1;
string seasonNumber = GetSeasonNumber(season, s);
foreach (var ep in episodes)
{
if (string.IsNullOrEmpty(ep.Hash))
continue;
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {index}" : ep.Title;
string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(ep.Hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
episode_tpl.Append(episodeName, title ?? original_title, seasonNumber, index.ToString("D2"), accsArgs(callUrl), "call");
index++;
}
return rjson ? Content(episode_tpl.ToJson(), "application/json; charset=utf-8") : Content(episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
else
{
string hash = project.Hash;
if (string.IsNullOrEmpty(hash) && project.Episodes.Count > 0)
hash = project.Episodes.FirstOrDefault(e => !string.IsNullOrEmpty(e.Hash))?.Hash;
if (string.IsNullOrEmpty(hash))
return OnError("starlight", proxyManager);
string callUrl = $"{host}/starlight/play?hash={HttpUtility.UrlEncode(hash)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
var movie_tpl = new MovieTpl(title, original_title, 1);
movie_tpl.Append(string.IsNullOrEmpty(title) ? "StarLight" : title, accsArgs(callUrl), "call");
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
[HttpGet]
[Route("starlight/play")]
async public Task<ActionResult> Play(string hash, string title)
{
if (string.IsNullOrEmpty(hash))
return OnError("starlight", proxyManager);
var init = await loadKit(ModInit.StarLight);
if (!init.enable)
return Forbid();
var invoke = new StarLightInvoke(init, hybridCache, OnLog, proxyManager);
var result = await invoke.ResolveStream(hash);
if (result == null || string.IsNullOrEmpty(result.Stream))
return OnError("starlight", proxyManager);
string streamUrl = HostStreamProxy(init, accsArgs(result.Stream), proxy: proxyManager.Get());
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? result.Name ?? ""}\"}}";
return Content(jsonResult, "application/json; charset=utf-8");
}
private static string GetSeasonNumber(SeasonInfo season, int fallbackIndex)
{
if (season?.Title == null)
return (fallbackIndex + 1).ToString();
var digits = new string(season.Title.Where(char.IsDigit).ToArray());
return string.IsNullOrEmpty(digits) ? (fallbackIndex + 1).ToString() : digits;
}
}
}

View File

@ -1,35 +0,0 @@
using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace StarLight
{
public class ModInit
{
public static OnlinesSettings StarLight;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
StarLight = new OnlinesSettings("StarLight", "https://tp-back.starlight.digital", streamproxy: false, useproxy: false)
{
displayname = "StarLight",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
StarLight = ModuleInvoke.Conf("StarLight", StarLight).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("starlight");
}
}
}

View File

@ -1,47 +0,0 @@
using System.Collections.Generic;
namespace StarLight.Models
{
public class SearchResult
{
public string Title { get; set; }
public string Type { get; set; }
public string Href { get; set; }
public string Channel { get; set; }
public string Project { get; set; }
}
public class SeasonInfo
{
public string Title { get; set; }
public string Slug { get; set; }
}
public class EpisodeInfo
{
public string Title { get; set; }
public string Hash { get; set; }
public string VideoSlug { get; set; }
public string Date { get; set; }
public string SeasonSlug { get; set; }
}
public class ProjectInfo
{
public string Title { get; set; }
public string Description { get; set; }
public string Poster { get; set; }
public string Hash { get; set; }
public string Type { get; set; }
public string Channel { get; set; }
public List<SeasonInfo> Seasons { get; set; } = new();
public List<EpisodeInfo> Episodes { get; set; } = new();
}
public class StreamResult
{
public string Stream { get; set; }
public string Poster { get; set; }
public string Name { get; set; }
}
}

View File

@ -1,29 +0,0 @@
using Shared.Models.Base;
using System;
using System.Collections.Generic;
namespace StarLight
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
if (!string.Equals(original_language, "uk", StringComparison.OrdinalIgnoreCase))
return online;
var init = ModInit.StarLight;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/starlight";
online.Add((init.displayname, url, "starlight", init.displayindex));
}
return online;
}
}
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,361 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using StarLight.Models;
namespace StarLight
{
public class StarLightInvoke
{
private const string PlayerApi = "https://vcms-api2.starlight.digital/player-api";
private const string PlayerReferer = "https://teleportal.ua/";
private const string Language = "ua";
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private readonly OnlinesSettings _init;
private readonly HybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
public StarLightInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<SearchResult>> Search(string title, string original_title)
{
string query = !string.IsNullOrEmpty(title) ? title : original_title;
if (string.IsNullOrEmpty(query))
return null;
string memKey = $"StarLight:search:{query}";
if (_hybridCache.TryGetValue(memKey, out List<SearchResult> cached))
return cached;
string url = $"{_init.host}/{Language}/live-search?q={HttpUtility.UrlEncode(query)}";
if (IsNotAllowedHost(url))
return null;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
try
{
_onLog?.Invoke($"StarLight search: {url}");
string payload = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(payload))
return null;
var results = new List<SearchResult>();
using var document = JsonDocument.Parse(payload);
if (document.RootElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in document.RootElement.EnumerateArray())
{
string typeSlug = item.TryGetProperty("typeSlug", out var typeProp) ? typeProp.GetString() : null;
string channelSlug = item.TryGetProperty("channelSlug", out var channelProp) ? channelProp.GetString() : null;
string projectSlug = item.TryGetProperty("projectSlug", out var projectProp) ? projectProp.GetString() : null;
if (string.IsNullOrEmpty(typeSlug) || string.IsNullOrEmpty(channelSlug) || string.IsNullOrEmpty(projectSlug))
continue;
string href = $"{_init.host}/{Language}/{typeSlug}/{channelSlug}/{projectSlug}";
results.Add(new SearchResult
{
Title = item.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null,
Type = typeSlug,
Href = href,
Channel = channelSlug,
Project = projectSlug
});
}
}
if (results.Count > 0)
_hybridCache.Set(memKey, results, cacheTime(15, init: _init));
return results;
}
catch (Exception ex)
{
_onLog?.Invoke($"StarLight search error: {ex.Message}");
return null;
}
}
public async Task<ProjectInfo> GetProject(string href)
{
if (string.IsNullOrEmpty(href))
return null;
string memKey = $"StarLight:project:{href}";
if (_hybridCache.TryGetValue(memKey, out ProjectInfo cached))
return cached;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
try
{
if (IsNotAllowedHost(href))
return null;
_onLog?.Invoke($"StarLight project: {href}");
string payload = await Http.Get(href, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(payload))
return null;
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;
var project = new ProjectInfo
{
Title = root.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null,
Description = root.TryGetProperty("description", out var descProp) ? descProp.GetString() : null,
Poster = NormalizeImage(root.TryGetProperty("image", out var imageProp) ? imageProp.GetString() : null),
Hash = root.TryGetProperty("hash", out var hashProp) ? hashProp.GetString() : null,
Type = root.TryGetProperty("typeSlug", out var typeProp) ? typeProp.GetString() : null,
Channel = root.TryGetProperty("channelTitle", out var channelProp) ? channelProp.GetString() : null
};
if (root.TryGetProperty("seasons", out var seasonsListProp) && seasonsListProp.ValueKind == JsonValueKind.Array)
{
foreach (var seasonItem in seasonsListProp.EnumerateArray())
{
string seasonTitle = seasonItem.TryGetProperty("title", out var sTitle) ? sTitle.GetString() : null;
string seasonSlug = seasonItem.TryGetProperty("seasonSlug", out var sSlug) ? sSlug.GetString() : null;
AddSeason(project, seasonTitle, seasonSlug);
}
}
if (root.TryGetProperty("seasonsGallery", out var seasonsProp) && seasonsProp.ValueKind == JsonValueKind.Array)
{
foreach (var seasonItem in seasonsProp.EnumerateArray())
{
string seasonTitle = seasonItem.TryGetProperty("title", out var sTitle) ? sTitle.GetString() : null;
string seasonSlug = seasonItem.TryGetProperty("seasonSlug", out var sSlug) ? sSlug.GetString() : null;
AddSeason(project, seasonTitle, seasonSlug);
if (seasonItem.TryGetProperty("items", out var itemsProp) && itemsProp.ValueKind == JsonValueKind.Array)
{
foreach (var item in itemsProp.EnumerateArray())
{
project.Episodes.Add(new EpisodeInfo
{
Title = item.TryGetProperty("title", out var eTitle) ? eTitle.GetString() : null,
Hash = item.TryGetProperty("hash", out var eHash) ? eHash.GetString() : null,
VideoSlug = item.TryGetProperty("videoSlug", out var eSlug) ? eSlug.GetString() : null,
Date = item.TryGetProperty("dateOfBroadcast", out var eDate) ? eDate.GetString() : (item.TryGetProperty("timeUploadVideo", out var eDate2) ? eDate2.GetString() : null),
SeasonSlug = seasonSlug
});
}
}
}
}
await LoadMissingSeasonEpisodes(project, href, headers);
_hybridCache.Set(memKey, project, cacheTime(10, init: _init));
return project;
}
catch (Exception ex)
{
_onLog?.Invoke($"StarLight project error: {ex.Message}");
return null;
}
}
private async Task LoadMissingSeasonEpisodes(ProjectInfo project, string href, List<HeadersModel> headers)
{
if (project == null || string.IsNullOrEmpty(href))
return;
var missing = project.Seasons
.Where(s => !string.IsNullOrEmpty(s.Slug))
.Where(s => !project.Episodes.Any(e => e.SeasonSlug == s.Slug))
.ToList();
foreach (var seasonInfo in missing)
{
string seasonUrl = $"{href}/{seasonInfo.Slug}";
try
{
_onLog?.Invoke($"StarLight season: {seasonUrl}");
string payload = await Http.Get(seasonUrl, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(payload))
continue;
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;
if (root.TryGetProperty("items", out var itemsProp) && itemsProp.ValueKind == JsonValueKind.Array)
{
foreach (var item in itemsProp.EnumerateArray())
{
string hash = item.TryGetProperty("hash", out var eHash) ? eHash.GetString() : null;
if (string.IsNullOrEmpty(hash))
continue;
if (project.Episodes.Any(e => e.SeasonSlug == seasonInfo.Slug && e.Hash == hash))
continue;
project.Episodes.Add(new EpisodeInfo
{
Title = item.TryGetProperty("title", out var eTitle) ? eTitle.GetString() : null,
Hash = hash,
VideoSlug = item.TryGetProperty("videoSlug", out var eSlug) ? eSlug.GetString() : null,
Date = item.TryGetProperty("dateOfBroadcast", out var eDate) ? eDate.GetString() : (item.TryGetProperty("timeUploadVideo", out var eDate2) ? eDate2.GetString() : null),
SeasonSlug = seasonInfo.Slug
});
}
}
}
catch (Exception ex)
{
_onLog?.Invoke($"StarLight season error: {ex.Message}");
}
}
}
private static void AddSeason(ProjectInfo project, string title, string slug)
{
if (project == null || string.IsNullOrEmpty(slug))
return;
if (project.Seasons.Any(s => s.Slug == slug))
return;
project.Seasons.Add(new SeasonInfo { Title = title, Slug = slug });
}
public List<EpisodeInfo> GetEpisodes(ProjectInfo project, string seasonSlug)
{
if (project == null || project.Seasons.Count == 0)
return new List<EpisodeInfo>();
if (string.IsNullOrEmpty(seasonSlug))
return project.Episodes;
return project.Episodes.Where(e => e.SeasonSlug == seasonSlug).ToList();
}
public async Task<StreamResult> ResolveStream(string hash)
{
if (string.IsNullOrEmpty(hash))
return null;
string url = $"{PlayerApi}/{hash}?referer={HttpUtility.UrlEncode(PlayerReferer)}&lang={Language}";
if (IsNotAllowedHost(url))
return null;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", PlayerReferer)
};
try
{
_onLog?.Invoke($"StarLight stream: {url}");
string payload = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(payload))
return null;
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;
string stream = null;
if (root.TryGetProperty("video", out var videoProp) && videoProp.ValueKind == JsonValueKind.Array)
{
var video = videoProp.EnumerateArray().FirstOrDefault();
if (video.ValueKind != JsonValueKind.Undefined)
{
if (video.TryGetProperty("mediaHlsNoAdv", out var hlsNoAdv))
stream = hlsNoAdv.GetString();
if (string.IsNullOrEmpty(stream) && video.TryGetProperty("mediaHls", out var hls))
stream = hls.GetString();
if (string.IsNullOrEmpty(stream) && video.TryGetProperty("media", out var mediaProp) && mediaProp.ValueKind == JsonValueKind.Array)
{
var media = mediaProp.EnumerateArray().FirstOrDefault();
if (media.TryGetProperty("url", out var mediaUrl))
stream = mediaUrl.GetString();
}
}
}
if (string.IsNullOrEmpty(stream))
return null;
return new StreamResult
{
Stream = stream,
Poster = root.TryGetProperty("poster", out var posterProp) ? posterProp.GetString() : null,
Name = root.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null
};
}
catch (Exception ex)
{
_onLog?.Invoke($"StarLight stream error: {ex.Message}");
return null;
}
}
private string NormalizeImage(string path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
return IsNotAllowedHost(path) ? string.Empty : path;
return IsNotAllowedHost(_init.host) ? string.Empty : $"{_init.host}{path}";
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
}
}

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "StarLight.ModInit",
"online": "StarLight.OnlineApi"
}

View File

@ -1,141 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Templates;
using UAKino.Models;
namespace UAKino.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.UAKino);
}
[HttpGet]
[Route("uakino")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, bool rjson = false, string href = null)
{
var init = await loadKit(ModInit.UAKino);
if (!init.enable)
return Forbid();
var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager);
string itemUrl = href;
if (string.IsNullOrEmpty(itemUrl))
{
var searchResults = await invoke.Search(title, original_title, serial);
if (searchResults == null || searchResults.Count == 0)
return OnError("uakino", proxyManager);
if (searchResults.Count > 1)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
string link = $"{host}/uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial={serial}&href={HttpUtility.UrlEncode(res.Url)}";
similar_tpl.Append(res.Title, string.Empty, string.Empty, link, res.Poster);
}
return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
}
itemUrl = searchResults[0].Url;
}
if (serial == 1)
{
var playlist = await invoke.GetPlaylist(itemUrl);
if (playlist == null || playlist.Count == 0)
return OnError("uakino", proxyManager);
var voiceGroups = playlist
.GroupBy(p => string.IsNullOrEmpty(p.Voice) ? "Основне" : p.Voice)
.Select(g => (key: g.Key, episodes: g.ToList()))
.ToList();
if (voiceGroups.Count == 0)
return OnError("uakino", proxyManager);
if (string.IsNullOrEmpty(t))
t = voiceGroups.First().key;
var voice_tpl = new VoiceTpl();
foreach (var voice in voiceGroups)
{
string voiceLink = $"{host}/uakino?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&t={HttpUtility.UrlEncode(voice.key)}&href={HttpUtility.UrlEncode(itemUrl)}";
voice_tpl.Append(voice.key, voice.key == t, voiceLink);
}
var selected = voiceGroups.FirstOrDefault(v => v.key == t);
if (selected.episodes == null || selected.episodes.Count == 0)
return OnError("uakino", proxyManager);
var episode_tpl = new EpisodeTpl();
int index = 1;
foreach (var ep in selected.episodes.OrderBy(e => UAKinoInvoke.TryParseEpisodeNumber(e.Title) ?? int.MaxValue))
{
int episodeNumber = UAKinoInvoke.TryParseEpisodeNumber(ep.Title) ?? index;
string episodeName = string.IsNullOrEmpty(ep.Title) ? $"Епізод {episodeNumber}" : ep.Title;
string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(ep.Url)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
episode_tpl.Append(episodeName, title ?? original_title, "1", episodeNumber.ToString("D2"), accsArgs(callUrl), "call");
index++;
}
if (rjson)
return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8");
return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
else
{
string playerUrl = await invoke.GetPlayerUrl(itemUrl);
if (string.IsNullOrEmpty(playerUrl))
{
var playlist = await invoke.GetPlaylist(itemUrl);
playerUrl = playlist?.FirstOrDefault()?.Url;
}
if (string.IsNullOrEmpty(playerUrl))
return OnError("uakino", proxyManager);
var movie_tpl = new MovieTpl(title, original_title);
string callUrl = $"{host}/uakino/play?url={HttpUtility.UrlEncode(playerUrl)}&title={HttpUtility.UrlEncode(title ?? original_title)}";
movie_tpl.Append(string.IsNullOrEmpty(title) ? "UAKino" : title, accsArgs(callUrl), "call");
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
[HttpGet]
[Route("uakino/play")]
async public Task<ActionResult> Play(string url, string title)
{
if (string.IsNullOrEmpty(url))
return OnError("uakino", proxyManager);
var init = await loadKit(ModInit.UAKino);
if (!init.enable)
return Forbid();
var invoke = new UAKinoInvoke(init, hybridCache, OnLog, proxyManager);
var result = await invoke.ParsePlayer(url);
if (result == null || string.IsNullOrEmpty(result.File))
return OnError("uakino", proxyManager);
string streamUrl = HostStreamProxy(init, accsArgs(result.File), proxy: proxyManager.Get());
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? ""}\"}}";
return Content(jsonResult, "application/json; charset=utf-8");
}
}
}

View File

@ -1,35 +0,0 @@
using Shared;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace UAKino
{
public class ModInit
{
public static OnlinesSettings UAKino;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
UAKino = new OnlinesSettings("UAKino", "https://uakino.best", streamproxy: false, useproxy: false)
{
displayname = "UAKino",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "",
password = "",
list = new string[] { "socks5://ip:port" }
}
};
UAKino = ModuleInvoke.Conf("UAKino", UAKino).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("uakino");
}
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
namespace UAKino.Models
{
public class SearchResult
{
public string Title { get; set; }
public string Url { get; set; }
public string Poster { get; set; }
public string Season { get; set; }
}
public class PlaylistItem
{
public string Title { get; set; }
public string Url { get; set; }
public string Voice { get; set; }
}
public class SubtitleInfo
{
public string Lang { get; set; }
public string Url { get; set; }
}
public class PlayerResult
{
public string File { get; set; }
public List<SubtitleInfo> Subtitles { get; set; } = new();
}
}

View File

@ -1,25 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace UAKino
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.UAKino;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/uakino";
online.Add((init.displayname, url, "uakino", init.displayindex));
}
return online;
}
}
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,509 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using HtmlAgilityPack;
using Shared;
using Shared.Engine;
using Shared.Models;
using Shared.Models.Online.Settings;
using UAKino.Models;
namespace UAKino
{
public class UAKinoInvoke
{
private const string PlaylistPath = "/engine/ajax/playlists.php";
private const string PlaylistField = "playlist";
private const string BlacklistRegex = "(/news/)|(/franchise/)";
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private readonly OnlinesSettings _init;
private readonly HybridCache _hybridCache;
private readonly Action<string> _onLog;
private readonly ProxyManager _proxyManager;
public UAKinoInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<SearchResult>> Search(string title, string original_title, int serial)
{
var queries = new List<string>();
if (!string.IsNullOrEmpty(title))
queries.Add(title);
if (!string.IsNullOrEmpty(original_title) && !queries.Contains(original_title))
queries.Add(original_title);
if (queries.Count == 0)
return null;
string memKey = $"UAKino:search:{string.Join("|", queries)}:{serial}";
if (_hybridCache.TryGetValue(memKey, out List<SearchResult> cached))
return cached;
var results = new List<SearchResult>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var query in queries)
{
try
{
string searchUrl = $"{_init.host}/index.php?do=search&subaction=search&story={HttpUtility.UrlEncode(query)}";
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
_onLog?.Invoke($"UAKino search: {searchUrl}");
string html = await GetString(searchUrl, headers);
if (string.IsNullOrEmpty(html))
continue;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var nodes = doc.DocumentNode.SelectNodes("//div[contains(@class,'movie-item') and contains(@class,'short-item')]");
if (nodes == null)
continue;
foreach (var node in nodes)
{
var titleNode = node.SelectSingleNode(".//a[contains(@class,'movie-title')] | .//a[contains(@class,'full-movie')]");
string itemTitle = CleanText(titleNode?.InnerText);
string href = NormalizeUrl(titleNode?.GetAttributeValue("href", ""));
if (string.IsNullOrEmpty(itemTitle))
{
var altTitle = node.SelectSingleNode(".//div[contains(@class,'full-movie-title')]");
itemTitle = CleanText(altTitle?.InnerText);
}
if (string.IsNullOrEmpty(itemTitle) || string.IsNullOrEmpty(href) || IsBlacklisted(href))
continue;
if (serial == 1 && !IsSeriesUrl(href))
continue;
if (serial == 0 && !IsMovieUrl(href))
continue;
string seasonText = CleanText(node.SelectSingleNode(".//div[contains(@class,'full-season')]")?.InnerText);
if (!string.IsNullOrEmpty(seasonText) && !itemTitle.Contains(seasonText, StringComparison.OrdinalIgnoreCase))
itemTitle = $"{itemTitle} ({seasonText})";
string poster = ExtractPoster(node);
if (seen.Contains(href))
continue;
seen.Add(href);
results.Add(new SearchResult
{
Title = itemTitle,
Url = href,
Poster = poster,
Season = seasonText
});
}
if (results.Count > 0)
break;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino search error: {ex.Message}");
}
}
if (results.Count > 0)
_hybridCache.Set(memKey, results, cacheTime(20, init: _init));
return results;
}
public async Task<List<PlaylistItem>> GetPlaylist(string href)
{
string newsId = ExtractNewsId(href);
if (string.IsNullOrEmpty(newsId))
return null;
string memKey = $"UAKino:playlist:{newsId}";
if (_hybridCache.TryGetValue(memKey, out List<PlaylistItem> cached))
return cached;
string url = BuildPlaylistUrl(newsId);
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", href ?? _init.host),
new HeadersModel("X-Requested-With", "XMLHttpRequest")
};
try
{
_onLog?.Invoke($"UAKino playlist: {url}");
string payload = await GetString(url, headers);
if (string.IsNullOrEmpty(payload))
return null;
using var document = JsonDocument.Parse(payload);
if (!document.RootElement.TryGetProperty("success", out var successProp) || !successProp.GetBoolean())
return null;
if (!document.RootElement.TryGetProperty("response", out var responseProp))
return null;
string html = responseProp.GetString();
if (string.IsNullOrEmpty(html))
return null;
var items = ParsePlaylistHtml(html);
if (items.Count > 0)
_hybridCache.Set(memKey, items, cacheTime(10, init: _init));
return items;
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino playlist error: {ex.Message}");
return null;
}
}
public async Task<string> GetPlayerUrl(string href)
{
if (string.IsNullOrEmpty(href))
return null;
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
try
{
_onLog?.Invoke($"UAKino movie page: {href}");
string html = await GetString(href, headers);
if (string.IsNullOrEmpty(html))
return null;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var iframe = doc.DocumentNode.SelectSingleNode("//iframe[@id='pre']");
if (iframe == null)
return null;
string src = iframe.GetAttributeValue("src", "");
if (string.IsNullOrEmpty(src))
src = iframe.GetAttributeValue("data-src", "");
return NormalizeUrl(src);
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino player url error: {ex.Message}");
return null;
}
}
public async Task<PlayerResult> ParsePlayer(string url)
{
if (string.IsNullOrEmpty(url))
return null;
if (LooksLikeDirectStream(url))
{
return new PlayerResult { File = url };
}
var headers = new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", _init.host)
};
try
{
_onLog?.Invoke($"UAKino parse player: {url}");
string html = await GetString(url, headers);
if (string.IsNullOrEmpty(html))
return null;
string file = ExtractPlayerFile(html);
if (string.IsNullOrEmpty(file))
return null;
return new PlayerResult
{
File = NormalizeUrl(file),
Subtitles = ExtractSubtitles(html)
};
}
catch (Exception ex)
{
_onLog?.Invoke($"UAKino parse player error: {ex.Message}");
return null;
}
}
private async Task<string> GetString(string url, List<HeadersModel> headers, int timeoutSeconds = 15)
{
if (IsNotAllowedHost(url))
return null;
var handler = new SocketsHttpHandler
{
AllowAutoRedirect = true,
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip | DecompressionMethods.Deflate,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (_, _, _, _) => true,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
}
};
var proxy = _proxyManager.Get();
if (proxy != null)
{
handler.UseProxy = true;
handler.Proxy = proxy;
}
else
{
handler.UseProxy = false;
}
using var client = new HttpClient(handler);
using var req = new HttpRequestMessage(HttpMethod.Get, url);
if (headers != null)
{
foreach (var h in headers)
req.Headers.TryAddWithoutValidation(h.name, h.val);
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)));
using var response = await client.SendAsync(req, cts.Token).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
}
private List<PlaylistItem> ParsePlaylistHtml(string html)
{
var items = new List<PlaylistItem>();
var doc = new HtmlDocument();
doc.LoadHtml(html);
var nodes = doc.DocumentNode.SelectNodes("//li[@data-file]");
if (nodes == null)
return items;
foreach (var node in nodes)
{
string dataFile = node.GetAttributeValue("data-file", "");
if (string.IsNullOrEmpty(dataFile))
continue;
string title = CleanText(node.InnerText);
string voice = node.GetAttributeValue("data-voice", "");
items.Add(new PlaylistItem
{
Title = string.IsNullOrEmpty(title) ? "Episode" : title,
Url = NormalizeUrl(dataFile),
Voice = voice
});
}
return items;
}
private string BuildPlaylistUrl(string newsId)
{
long ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
return $"{_init.host}{PlaylistPath}?news_id={newsId}&xfield={PlaylistField}&time={ts}";
}
private static string ExtractNewsId(string href)
{
if (string.IsNullOrEmpty(href))
return null;
string tail = href.TrimEnd('/').Split('/').LastOrDefault();
if (string.IsNullOrEmpty(tail))
return null;
string newsId = tail.Split('-')[0];
return string.IsNullOrEmpty(newsId) ? null : newsId;
}
private static string ExtractPlayerFile(string html)
{
var match = Regex.Match(html, "file\\s*:\\s*['\"]([^'\"]+)['\"]", RegexOptions.IgnoreCase);
if (match.Success)
{
string value = match.Groups[1].Value.Trim();
if (!value.StartsWith("[", StringComparison.Ordinal))
return value;
}
var sourceMatch = Regex.Match(html, "<source[^>]+src=['\"]([^'\"]+)['\"]", RegexOptions.IgnoreCase);
if (sourceMatch.Success)
return sourceMatch.Groups[1].Value;
var m3u8Match = Regex.Match(html, "(https?://[^\"'\\s>]+\\.m3u8[^\"'\\s>]*)", RegexOptions.IgnoreCase);
if (m3u8Match.Success)
return m3u8Match.Groups[1].Value;
return null;
}
private List<SubtitleInfo> ExtractSubtitles(string html)
{
var subtitles = new List<SubtitleInfo>();
var match = Regex.Match(html, "subtitle\\s*:\\s*['\"]([^'\"]+)['\"]", RegexOptions.IgnoreCase);
if (!match.Success)
return subtitles;
string value = match.Groups[1].Value.Trim();
if (string.IsNullOrEmpty(value))
return subtitles;
if (value.StartsWith("[", StringComparison.Ordinal) && value.Contains(']'))
{
int endIdx = value.LastIndexOf(']');
string label = value.Substring(1, endIdx - 1).Trim();
string url = value[(endIdx + 1)..].Trim();
url = NormalizeUrl(url);
if (!string.IsNullOrEmpty(url))
subtitles.Add(new SubtitleInfo { Lang = string.IsNullOrEmpty(label) ? "unknown" : label, Url = url });
}
else if (value.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
subtitles.Add(new SubtitleInfo { Lang = "unknown", Url = value });
}
return subtitles;
}
private string NormalizeUrl(string url)
{
if (string.IsNullOrEmpty(url))
return string.Empty;
if (url.StartsWith("//"))
return IsNotAllowedHost($"https:{url}") ? string.Empty : $"https:{url}";
if (url.StartsWith("/"))
return IsNotAllowedHost(_init.host) ? string.Empty : $"{_init.host}{url}";
return IsNotAllowedHost(url) ? string.Empty : url;
}
private static bool LooksLikeDirectStream(string url)
{
return url.Contains(".m3u8", StringComparison.OrdinalIgnoreCase) || url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase);
}
private static bool IsBlacklisted(string url)
{
return Regex.IsMatch(url ?? string.Empty, BlacklistRegex, RegexOptions.IgnoreCase);
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
private static bool IsSeriesUrl(string url)
{
return url.Contains("/seriesss/") || url.Contains("/anime-series/") || url.Contains("/cartoonseries/");
}
private static bool IsMovieUrl(string url)
{
return url.Contains("/filmy/") || url.Contains("/anime-solo/") || url.Contains("/features/");
}
private string ExtractPoster(HtmlNode node)
{
var img = node.SelectSingleNode(".//img");
if (img == null)
return string.Empty;
string src = img.GetAttributeValue("src", "");
if (string.IsNullOrEmpty(src))
src = img.GetAttributeValue("data-src", "");
return NormalizeUrl(src);
}
private static string CleanText(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return HtmlEntity.DeEntitize(value).Trim();
}
private static int? ExtractEpisodeNumber(string title)
{
if (string.IsNullOrEmpty(title))
return null;
var match = Regex.Match(title, @"(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int number))
return number;
return null;
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
public static int? TryParseEpisodeNumber(string title)
{
return ExtractEpisodeNumber(title);
}
}
}

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "UAKino.ModInit",
"online": "UAKino.OnlineApi"
}

View File

@ -1,424 +0,0 @@
using Shared.Engine;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using Shared;
using Shared.Models.Templates;
using UaTUT.Models;
using System.Text.RegularExpressions;
using Shared.Models.Online.Settings;
using Shared.Models;
namespace UaTUT
{
[Route("uatut")]
public class UaTUTController : BaseOnlineController
{
ProxyManager proxyManager;
public UaTUTController()
{
proxyManager = new ProxyManager(ModInit.UaTUT);
}
[HttpGet]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int season = -1, bool rjson = false)
{
var init = await loadKit(ModInit.UaTUT);
if (!init.enable)
return OnError();
OnLog($"UaTUT: {title} (serial={serial}, s={s}, season={season}, t={t})");
var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager);
// Використовуємо кеш для пошуку, щоб уникнути дублювання запитів
string searchCacheKey = $"uatut:search:{imdb_id ?? original_title ?? title}";
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
{
return await invoke.Search(original_title ?? title, imdb_id);
});
if (searchResults == null || !searchResults.Any())
{
OnLog("UaTUT: No search results found");
return OnError();
}
if (serial == 1)
{
return await HandleSeries(searchResults, imdb_id, kinopoisk_id, title, original_title, year, s, season, t, rjson, invoke);
}
else
{
return await HandleMovie(searchResults, rjson, invoke);
}
}
private async Task<ActionResult> HandleSeries(List<SearchResult> searchResults, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, bool rjson, UaTUTInvoke invoke)
{
var init = ModInit.UaTUT;
// Фільтруємо тільки серіали та аніме
var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList();
if (!seriesResults.Any())
{
OnLog("UaTUT: No series found in search results");
return OnError();
}
if (s == -1) // Крок 1: Відображення списку серіалів
{
var season_tpl = new SeasonTpl();
for (int i = 0; i < seriesResults.Count; i++)
{
var series = seriesResults[i];
string seasonName = $"{series.Title} ({series.Year})";
string link = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={i}";
season_tpl.Append(seasonName, link, i.ToString());
}
OnLog($"UaTUT: generated {seriesResults.Count} series options");
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
else if (season == -1) // Крок 2: Відображення сезонів для вибраного серіалу
{
if (s >= seriesResults.Count)
return OnError();
var selectedSeries = seriesResults[s];
// Використовуємо кеш для уникнення повторних запитів
string cacheKey = $"uatut:player_data:{selectedSeries.Id}";
var playerData = await InvokeCache<PlayerData>(cacheKey, TimeSpan.FromMinutes(10), async () =>
{
return await GetPlayerDataCached(selectedSeries, invoke);
});
if (playerData?.Voices == null || !playerData.Voices.Any())
return OnError();
// Використовуємо першу озвучку для отримання списку сезонів
var firstVoice = playerData.Voices.First();
var season_tpl = new SeasonTpl();
for (int i = 0; i < firstVoice.Seasons.Count; i++)
{
var seasonItem = firstVoice.Seasons[i];
string seasonName = seasonItem.Title ?? $"Сезон {i + 1}";
string link = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={i}";
season_tpl.Append(seasonName, link, i.ToString());
}
OnLog($"UaTUT: found {firstVoice.Seasons.Count} seasons");
return rjson ? Content(season_tpl.ToJson(), "application/json; charset=utf-8") : Content(season_tpl.ToHtml(), "text/html; charset=utf-8");
}
else // Крок 3: Відображення озвучок та епізодів для вибраного сезону
{
if (s >= seriesResults.Count)
return OnError();
var selectedSeries = seriesResults[s];
// Використовуємо той самий кеш
string cacheKey = $"uatut:player_data:{selectedSeries.Id}";
var playerData = await InvokeCache<PlayerData>(cacheKey, TimeSpan.FromMinutes(10), async () =>
{
return await GetPlayerDataCached(selectedSeries, invoke);
});
if (playerData?.Voices == null || !playerData.Voices.Any())
return OnError();
// Перевіряємо чи існує вибраний сезон
if (season >= playerData.Voices.First().Seasons.Count)
return OnError();
var voice_tpl = new VoiceTpl();
var episode_tpl = new EpisodeTpl();
// Автоматично вибираємо першу озвучку якщо не вибрана
string selectedVoice = t;
if (string.IsNullOrEmpty(selectedVoice) && playerData.Voices.Any())
{
selectedVoice = "0"; // Перша озвучка
}
// Додаємо всі озвучки
for (int i = 0; i < playerData.Voices.Count; i++)
{
var voice = playerData.Voices[i];
string voiceName = voice.Name ?? $"Озвучка {i + 1}";
string voiceLink = $"{host}/uatut?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&serial=1&s={s}&season={season}&t={i}";
bool isActive = selectedVoice == i.ToString();
voice_tpl.Append(voiceName, isActive, voiceLink);
}
// Додаємо епізоди тільки для вибраного сезону та озвучки
if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int voiceIndex) && voiceIndex < playerData.Voices.Count)
{
var selectedVoiceData = playerData.Voices[voiceIndex];
if (season < selectedVoiceData.Seasons.Count)
{
var selectedSeason = selectedVoiceData.Seasons[season];
// Сортуємо епізоди та додаємо правильну нумерацію
var sortedEpisodes = selectedSeason.Episodes.OrderBy(e => ExtractEpisodeNumber(e.Title)).ToList();
for (int i = 0; i < sortedEpisodes.Count; i++)
{
var episode = sortedEpisodes[i];
string episodeName = episode.Title;
string episodeFile = episode.File;
if (!string.IsNullOrEmpty(episodeFile))
{
// Створюємо прямий лінк на епізод через play action
string episodeLink = $"{host}/uatut/play?imdb_id={imdb_id}&kinopoisk_id={kinopoisk_id}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&year={year}&s={s}&season={season}&t={selectedVoice}&episodeId={episode.Id}";
// Використовуємо правильний синтаксис EpisodeTpl.Append без poster параметра
episode_tpl.Append(episodeName, title ?? original_title, season.ToString(), (i + 1).ToString("D2"), episodeLink, "call");
}
}
}
}
int voiceCount = playerData.Voices.Count;
int episodeCount = 0;
if (!string.IsNullOrEmpty(selectedVoice) && int.TryParse(selectedVoice, out int vIndex) && vIndex < playerData.Voices.Count)
{
var selectedVoiceData = playerData.Voices[vIndex];
if (season < selectedVoiceData.Seasons.Count)
{
episodeCount = selectedVoiceData.Seasons[season].Episodes.Count;
}
}
OnLog($"UaTUT: generated {voiceCount} voices, {episodeCount} episodes");
if (rjson)
return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8");
return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
// Допоміжний метод для кешованого отримання даних плеєра
private async Task<PlayerData> GetPlayerDataCached(SearchResult selectedSeries, UaTUTInvoke invoke)
{
var pageContent = await invoke.GetMoviePageContent(selectedSeries.Id);
if (string.IsNullOrEmpty(pageContent))
return null;
var playerUrl = await invoke.GetPlayerUrl(pageContent);
if (string.IsNullOrEmpty(playerUrl))
return null;
return await invoke.GetPlayerData(playerUrl);
}
// Допоміжний метод для витягування номера епізоду з назви
private int ExtractEpisodeNumber(string title)
{
if (string.IsNullOrEmpty(title))
return 0;
var match = Regex.Match(title, @"(\d+)");
return match.Success ? int.Parse(match.Groups[1].Value) : 0;
}
private async Task<ActionResult> HandleMovie(List<SearchResult> searchResults, bool rjson, UaTUTInvoke invoke)
{
var init = ModInit.UaTUT;
// Фільтруємо тільки фільми
var movieResults = searchResults.Where(r => r.Category == "Фільм").ToList();
if (!movieResults.Any())
{
OnLog("UaTUT: No movies found in search results");
return OnError();
}
var movie_tpl = new MovieTpl(title: "UaTUT Movies", original_title: "UaTUT Movies");
foreach (var movie in movieResults)
{
var pageContent = await invoke.GetMoviePageContent(movie.Id);
if (string.IsNullOrEmpty(pageContent))
continue;
var playerUrl = await invoke.GetPlayerUrl(pageContent);
if (string.IsNullOrEmpty(playerUrl))
continue;
var playerData = await invoke.GetPlayerData(playerUrl);
if (playerData?.File == null)
continue;
string movieName = $"{movie.Title} ({movie.Year})";
string movieLink = $"{host}/uatut/play/movie?imdb_id={movie.Id}&title={HttpUtility.UrlEncode(movie.Title)}&year={movie.Year}";
movie_tpl.Append(movieName, movieLink, "call");
}
if (movie_tpl.IsEmpty())
{
OnLog("UaTUT: No playable movies found");
return OnError();
}
OnLog($"UaTUT: found {movieResults.Count} movies");
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
}
[HttpGet]
[Route("play/movie")]
async public Task<ActionResult> PlayMovie(long imdb_id, string title, int year, bool play = false, bool rjson = false)
{
var init = await loadKit(ModInit.UaTUT);
if (!init.enable)
return OnError();
OnLog($"UaTUT PlayMovie: {title} ({year}) play={play}");
var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager);
// Використовуємо кеш для пошуку
string searchCacheKey = $"uatut:search:{title}";
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
{
return await invoke.Search(title, null);
});
if (searchResults == null || !searchResults.Any())
{
OnLog("UaTUT PlayMovie: No search results found");
return OnError();
}
// Шукаємо фільм за ID
var movie = searchResults.FirstOrDefault(r => r.Id == imdb_id.ToString() && r.Category == "Фільм");
if (movie == null)
{
OnLog("UaTUT PlayMovie: Movie not found");
return OnError();
}
var pageContent = await invoke.GetMoviePageContent(movie.Id);
if (string.IsNullOrEmpty(pageContent))
return OnError();
var playerUrl = await invoke.GetPlayerUrl(pageContent);
if (string.IsNullOrEmpty(playerUrl))
return OnError();
var playerData = await invoke.GetPlayerData(playerUrl);
if (playerData?.File == null)
return OnError();
OnLog($"UaTUT PlayMovie: Found direct file: {playerData.File}");
string streamUrl = HostStreamProxy(init, playerData.File);
// Якщо play=true, робимо Redirect, інакше повертаємо JSON
if (play)
return Redirect(streamUrl);
else
return Content(VideoTpl.ToJson("play", streamUrl, title), "application/json; charset=utf-8");
}
[HttpGet]
[Route("play")]
async public Task<ActionResult> Play(long id, string imdb_id, long kinopoisk_id, string title, string original_title, int year, int s, int season, string t, string episodeId, bool play = false, bool rjson = false)
{
var init = await loadKit(ModInit.UaTUT);
if (!init.enable)
return OnError();
OnLog($"UaTUT Play: {title} (s={s}, season={season}, t={t}, episodeId={episodeId}) play={play}");
var invoke = new UaTUTInvoke(init, hybridCache, OnLog, proxyManager);
// Використовуємо кеш для пошуку
string searchCacheKey = $"uatut:search:{imdb_id ?? original_title ?? title}";
var searchResults = await InvokeCache<List<SearchResult>>(searchCacheKey, TimeSpan.FromMinutes(10), async () =>
{
return await invoke.Search(original_title ?? title, imdb_id);
});
if (searchResults == null || !searchResults.Any())
{
OnLog("UaTUT Play: No search results found");
return OnError();
}
// Фільтруємо тільки серіали та аніме
var seriesResults = searchResults.Where(r => r.Category == "Серіал" || r.Category == "Аніме").ToList();
if (!seriesResults.Any() || s >= seriesResults.Count)
{
OnLog("UaTUT Play: No series found or invalid series index");
return OnError();
}
var selectedSeries = seriesResults[s];
// Використовуємо той самий кеш як і в HandleSeries
string cacheKey = $"uatut:player_data:{selectedSeries.Id}";
var playerData = await InvokeCache<PlayerData>(cacheKey, TimeSpan.FromMinutes(10), async () =>
{
return await GetPlayerDataCached(selectedSeries, invoke);
});
if (playerData?.Voices == null || !playerData.Voices.Any())
{
OnLog("UaTUT Play: No player data or voices found");
return OnError();
}
// Знаходимо потрібний епізод в конкретному сезоні та озвучці
if (int.TryParse(t, out int voiceIndex) && voiceIndex < playerData.Voices.Count)
{
var selectedVoice = playerData.Voices[voiceIndex];
if (season >= 0 && season < selectedVoice.Seasons.Count)
{
var selectedSeasonData = selectedVoice.Seasons[season];
foreach (var episode in selectedSeasonData.Episodes)
{
if (episode.Id == episodeId && !string.IsNullOrEmpty(episode.File))
{
OnLog($"UaTUT Play: Found episode {episode.Title}, stream: {episode.File}");
string streamUrl = HostStreamProxy(init, episode.File);
string episodeTitle = $"{title ?? original_title} - {episode.Title}";
// Якщо play=true, робимо Redirect, інакше повертаємо JSON
if (play)
return Redirect(streamUrl);
else
return Content(VideoTpl.ToJson("play", streamUrl, episodeTitle), "application/json; charset=utf-8");
}
}
}
else
{
OnLog($"UaTUT Play: Invalid season {season}, available seasons: {selectedVoice.Seasons.Count}");
}
}
else
{
OnLog($"UaTUT Play: Invalid voice index {t}, available voices: {playerData.Voices.Count}");
}
OnLog("UaTUT Play: Episode not found");
return OnError();
}
}
}

View File

@ -1,38 +0,0 @@
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace UaTUT
{
public class ModInit
{
public static OnlinesSettings UaTUT;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
UaTUT = new OnlinesSettings("UaTUT", "https://uk.uatut.fun", streamproxy: false, useproxy: false)
{
displayname = "🇺🇦 UaTUT",
displayindex = 0,
apihost = "https://uk.uatut.fun/watch",
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "a",
password = "a",
list = new string[] { "socks5://IP:PORT" }
}
};
UaTUT = ModuleInvoke.Conf("UaTUT", UaTUT).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("uatut");
}
}
}

View File

@ -1,61 +0,0 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace UaTUT.Models
{
public class SearchResult
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("imdb_id")]
public string ImdbId { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("title_alt")]
public string TitleAlt { get; set; }
[JsonProperty("title_en")]
public string TitleEn { get; set; }
[JsonProperty("title_ru")]
public string TitleRu { get; set; }
[JsonProperty("year")]
public string Year { get; set; }
[JsonProperty("category")]
public string Category { get; set; }
}
public class PlayerData
{
public string File { get; set; }
public string Poster { get; set; }
public List<Voice> Voices { get; set; }
public List<Season> Seasons { get; set; } // Залишаємо для зворотної сумісності
}
public class Voice
{
public string Name { get; set; }
public List<Season> Seasons { get; set; }
}
public class Season
{
public string Title { get; set; }
public List<Episode> Episodes { get; set; }
}
public class Episode
{
public string Title { get; set; }
public string File { get; set; }
public string Id { get; set; }
public string Poster { get; set; }
public string Subtitle { get; set; }
}
}

View File

@ -1,26 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace UaTUT
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.UaTUT;
// UaTUT: змішаний контент (аніме + не-аніме) — завжди включати при enable && !rip
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/uatut";
online.Add((init.displayname, url, "uatut", init.displayindex));
}
return online;
}
}
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,283 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
using Shared.Engine;
using Shared.Models.Online.Settings;
using Shared.Models;
using UaTUT.Models;
namespace UaTUT
{
public class UaTUTInvoke
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private OnlinesSettings _init;
private HybridCache _hybridCache;
private Action<string> _onLog;
private ProxyManager _proxyManager;
public UaTUTInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<SearchResult>> Search(string query, string imdbId = null)
{
try
{
string searchUrl = $"{_init.apihost}/search.php";
// Поступовий пошук: спочатку по imdbId, потім по назві
if (!string.IsNullOrEmpty(imdbId))
{
var imdbResults = await PerformSearch(searchUrl, imdbId);
if (imdbResults?.Any() == true)
return imdbResults;
}
// Пошук по назві
if (!string.IsNullOrEmpty(query))
{
var titleResults = await PerformSearch(searchUrl, query);
return titleResults ?? new List<SearchResult>();
}
return new List<SearchResult>();
}
catch (Exception ex)
{
_onLog($"UaTUT Search error: {ex.Message}");
return new List<SearchResult>();
}
}
private async Task<List<SearchResult>> PerformSearch(string searchUrl, string query)
{
string url = $"{searchUrl}?q={HttpUtility.UrlEncode(query)}";
_onLog($"UaTUT searching: {url}");
if (IsNotAllowedHost(url))
return null;
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(response))
return null;
try
{
var results = JsonConvert.DeserializeObject<List<SearchResult>>(response);
_onLog($"UaTUT found {results?.Count ?? 0} results for query: {query}");
return results;
}
catch (Exception ex)
{
_onLog($"UaTUT parse error: {ex.Message}");
return null;
}
}
public async Task<string> GetMoviePageContent(string movieId)
{
try
{
string url = $"{_init.apihost}/{movieId}";
_onLog($"UaTUT getting movie page: {url}");
if (IsNotAllowedHost(url))
return null;
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
var response = await Http.Get(url, headers: headers, proxy: _proxyManager.Get());
return response;
}
catch (Exception ex)
{
_onLog($"UaTUT GetMoviePageContent error: {ex.Message}");
return null;
}
}
public async Task<string> GetPlayerUrl(string moviePageContent)
{
try
{
// Шукаємо iframe з id="vip-player" та class="tab-content"
var match = Regex.Match(moviePageContent, @"<iframe[^>]*id=[""']vip-player[""'][^>]*src=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
if (match.Success)
{
string playerUrl = match.Groups[1].Value;
_onLog($"UaTUT found player URL: {playerUrl}");
return playerUrl;
}
_onLog("UaTUT: vip-player iframe not found");
return null;
}
catch (Exception ex)
{
_onLog($"UaTUT GetPlayerUrl error: {ex.Message}");
return null;
}
}
public async Task<PlayerData> GetPlayerData(string playerUrl)
{
try
{
_onLog($"UaTUT getting player data from: {playerUrl}");
if (IsNotAllowedHost(playerUrl))
return null;
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") };
var response = await Http.Get(playerUrl, headers: headers, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(response))
return null;
return ParsePlayerData(response);
}
catch (Exception ex)
{
_onLog($"UaTUT GetPlayerData error: {ex.Message}");
return null;
}
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
private PlayerData ParsePlayerData(string playerHtml)
{
try
{
var playerData = new PlayerData();
// Для фільмів шукаємо прямий file
var fileMatch = Regex.Match(playerHtml, @"file:'([^']+)'", RegexOptions.IgnoreCase);
if (fileMatch.Success && !fileMatch.Groups[1].Value.StartsWith("["))
{
playerData.File = fileMatch.Groups[1].Value;
_onLog($"UaTUT found direct file: {playerData.File}");
// Шукаємо poster
var posterMatch = Regex.Match(playerHtml, @"poster:[""']([^""']+)[""']", RegexOptions.IgnoreCase);
if (posterMatch.Success)
playerData.Poster = posterMatch.Groups[1].Value;
return playerData;
}
// Для серіалів шукаємо JSON структуру з сезонами та озвучками
var jsonMatch = Regex.Match(playerHtml, @"file:'(\[.*?\])'", RegexOptions.Singleline);
if (jsonMatch.Success)
{
string jsonData = jsonMatch.Groups[1].Value;
_onLog($"UaTUT found JSON data for series");
playerData.Voices = ParseVoicesJson(jsonData);
return playerData;
}
_onLog("UaTUT: No player data found");
return null;
}
catch (Exception ex)
{
_onLog($"UaTUT ParsePlayerData error: {ex.Message}");
return null;
}
}
private List<Voice> ParseVoicesJson(string jsonData)
{
try
{
// Декодуємо JSON структуру озвучок
dynamic voicesData = JsonConvert.DeserializeObject(jsonData);
var voices = new List<Voice>();
if (voicesData != null)
{
foreach (var voiceGroup in voicesData)
{
var voice = new Voice
{
Name = voiceGroup.title?.ToString(),
Seasons = new List<Season>()
};
if (voiceGroup.folder != null)
{
foreach (var seasonData in voiceGroup.folder)
{
var season = new Season
{
Title = seasonData.title?.ToString(),
Episodes = new List<Episode>()
};
if (seasonData.folder != null)
{
foreach (var episodeData in seasonData.folder)
{
var episode = new Episode
{
Title = episodeData.title?.ToString(),
File = episodeData.file?.ToString(),
Id = episodeData.id?.ToString(),
Poster = episodeData.poster?.ToString(),
Subtitle = episodeData.subtitle?.ToString()
};
season.Episodes.Add(episode);
}
}
voice.Seasons.Add(season);
}
}
voices.Add(voice);
}
}
_onLog($"UaTUT parsed {voices.Count} voices");
return voices;
}
catch (Exception ex)
{
_onLog($"UaTUT ParseVoicesJson error: {ex.Message}");
return new List<Voice>();
}
}
}
}

View File

@ -1,7 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "UaTUT.ModInit",
"online": "UaTUT.OnlineApi"
}

View File

@ -1,358 +0,0 @@
using Shared.Models.Templates;
using Shared.Engine;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using System.Linq;
using HtmlAgilityPack;
using Shared;
using Shared.Models.Templates;
using System.Text.RegularExpressions;
using System.Text;
using Shared.Models.Online.Settings;
using Shared.Models;
using Uaflix.Models;
namespace Uaflix.Controllers
{
public class Controller : BaseOnlineController
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.UaFlix);
}
[HttpGet]
[Route("uaflix")]
async public Task<ActionResult> Index(long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email, string t, int s = -1, int e = -1, bool play = false, bool rjson = false, string href = null, bool checksearch = false)
{
var init = await loadKit(ModInit.UaFlix);
if (await IsBadInitialization(init))
return Forbid();
OnLog($"=== UAFLIX INDEX START ===");
OnLog($"Uaflix Index: title={title}, serial={serial}, s={s}, play={play}, href={href}, checksearch={checksearch}");
OnLog($"Uaflix Index: kinopoisk_id={kinopoisk_id}, imdb_id={imdb_id}, id={id}");
OnLog($"Uaflix Index: year={year}, source={source}, t={t}, e={e}, rjson={rjson}");
var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager);
// Обробка параметра checksearch - повертаємо спеціальну відповідь для валідації
if (checksearch)
{
try
{
string filmTitle = !string.IsNullOrEmpty(title) ? title : original_title;
string searchUrl = $"{init.host}/index.php?do=search&subaction=search&story={System.Web.HttpUtility.UrlEncode(filmTitle)}";
var headers = new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) };
if (IsNotAllowedHost(searchUrl))
return OnError("uaflix", proxyManager);
var searchHtml = await Http.Get(searchUrl, headers: headers, proxy: proxyManager.Get(), timeoutSeconds: 10);
// Швидка перевірка наявності результатів без повного парсингу
if (!string.IsNullOrEmpty(searchHtml) &&
(searchHtml.Contains("sres-wrap") || searchHtml.Contains("sres-item") || searchHtml.Contains("search-results")))
{
// Якщо знайдено контент, повертаємо "data-json=" для валідації
OnLog("checksearch: Content found, returning validation response");
OnLog("=== RETURN: checksearch validation (data-json=) ===");
return Content("data-json=", "text/plain; charset=utf-8");
}
else
{
// Якщо нічого не знайдено, повертаємо OnError
OnLog("checksearch: No content found");
OnLog("=== RETURN: checksearch OnError ===");
return OnError("uaflix", proxyManager);
}
}
catch (Exception ex)
{
OnLog($"checksearch error: {ex.Message}");
OnLog("=== RETURN: checksearch exception OnError ===");
return OnError("uaflix", proxyManager);
}
}
if (play)
{
// Визначаємо URL для парсингу - або з параметра t, або з episode_url
string urlToParse = !string.IsNullOrEmpty(t) ? t : Request.Query["episode_url"];
var playResult = await invoke.ParseEpisode(urlToParse);
if (playResult.streams != null && playResult.streams.Count > 0)
{
OnLog("=== RETURN: play redirect ===");
return Redirect(HostStreamProxy(init, accsArgs(playResult.streams.First().link)));
}
OnLog("=== RETURN: play no streams ===");
return Content("Uaflix", "text/html; charset=utf-8");
}
// Якщо є episode_url але немає play=true, це виклик для отримання інформації про стрім (для method: 'call')
string episodeUrl = Request.Query["episode_url"];
if (!string.IsNullOrEmpty(episodeUrl))
{
var playResult = await invoke.ParseEpisode(episodeUrl);
if (playResult.streams != null && playResult.streams.Count > 0)
{
// Повертаємо JSON з інформацією про стрім для методу 'play'
string streamUrl = HostStreamProxy(init, accsArgs(playResult.streams.First().link));
string jsonResult = $"{{\"method\":\"play\",\"url\":\"{streamUrl}\",\"title\":\"{title ?? original_title}\"}}";
OnLog($"=== RETURN: call method JSON for episode_url ===");
return Content(jsonResult, "application/json; charset=utf-8");
}
OnLog("=== RETURN: call method no streams ===");
return Content("Uaflix", "text/html; charset=utf-8");
}
string filmUrl = href;
if (string.IsNullOrEmpty(filmUrl))
{
var searchResults = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, title);
if (searchResults == null || searchResults.Count == 0)
{
OnLog("No search results found");
OnLog("=== RETURN: no search results OnError ===");
return OnError("uaflix", proxyManager);
}
// Для фільмів і серіалів показуємо вибір тільки якщо більше одного результату
if (searchResults.Count > 1)
{
var similar_tpl = new SimilarTpl(searchResults.Count);
foreach (var res in searchResults)
{
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={serial}&href={HttpUtility.UrlEncode(res.Url)}";
similar_tpl.Append(res.Title, res.Year.ToString(), string.Empty, link, res.PosterUrl);
}
OnLog($"=== RETURN: similar items ({searchResults.Count}) ===");
return rjson ? Content(similar_tpl.ToJson(), "application/json; charset=utf-8") : Content(similar_tpl.ToHtml(), "text/html; charset=utf-8");
}
filmUrl = searchResults[0].Url;
OnLog($"Auto-selected first search result: {filmUrl}");
}
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", proxyManager);
}
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: Вибір сезону
if (s == -1)
{
var allSeasons = structure.Voices
.SelectMany(v => v.Value.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)
{
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 ===");
return OnError("uaflix", proxyManager);
}
var season_tpl = new SeasonTpl(seasonsWithValidEpisodes.Count);
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)}";
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");
}
// s >= 0: Показуємо озвучки + епізоди
else if (s >= 0)
{
var voicesForSeason = structure.Voices
.Where(v => v.Value.Seasons.ContainsKey(s))
.Select(v => new { DisplayName = v.Key, Info = v.Value })
.ToList();
if (!voicesForSeason.Any())
{
OnLog($"No voices found for season {s}");
OnLog("=== RETURN: no voices for season OnError ===");
return OnError("uaflix", proxyManager);
}
// Автоматично вибираємо першу озвучку якщо не вказана
if (string.IsNullOrEmpty(t))
{
t = voicesForSeason[0].DisplayName;
OnLog($"Auto-selected first voice: {t}");
}
// Створюємо VoiceTpl з усіма озвучками
var voice_tpl = new VoiceTpl();
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 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", proxyManager);
}
if (!structure.Voices[t].Seasons.ContainsKey(s))
{
OnLog($"Season {s} not found for voice '{t}'");
OnLog("=== RETURN: season not found for voice OnError ===");
return OnError("uaflix", proxyManager);
}
var episodes = structure.Voices[t].Seasons[s];
var episode_tpl = new EpisodeTpl();
foreach (var ep in episodes)
{
// Для zetvideo-vod повертаємо URL епізоду з методом call
// Для ashdi/zetvideo-serial повертаємо готове посилання з play
var voice = structure.Voices[t];
if (voice.PlayerType == "zetvideo-vod" || voice.PlayerType == "ashdi-vod")
{
// Для zetvideo-vod та ashdi-vod використовуємо URL епізоду для виклику
// Потрібно передати URL епізоду в інший параметр, щоб не плутати з play=true
string callUrl = $"{host}/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,
title: title,
s: s.ToString(),
e: ep.Number.ToString(),
link: accsArgs(callUrl),
method: "call",
streamlink: accsArgs($"{callUrl}&play=true")
);
}
else
{
// Для багатосерійних плеєрів (ashdi-serial, zetvideo-serial) - пряме відтворення
string playUrl = HostStreamProxy(init, accsArgs(ep.File));
episode_tpl.Append(
name: ep.Title,
title: title,
s: s.ToString(),
e: ep.Number.ToString(),
link: playUrl
);
}
}
OnLog($"Created EpisodeTpl with {episodes.Count} episodes");
// Повертаємо VoiceTpl + EpisodeTpl разом
if (rjson)
{
OnLog($"=== RETURN: episode template with voices JSON ({episodes.Count} episodes) ===");
return Content(episode_tpl.ToJson(voice_tpl), "application/json; charset=utf-8");
}
else
{
OnLog($"=== RETURN: voice + episode template HTML ({episodes.Count} episodes) ===");
return Content(voice_tpl.ToHtml() + episode_tpl.ToHtml(), "text/html; charset=utf-8");
}
}
// Fallback: якщо жоден з умов не виконався
OnLog($"Fallback: s={s}, t={t}");
OnLog("=== RETURN: fallback OnError ===");
return OnError("uaflix", proxyManager);
}
else // Фільм
{
string link = $"{host}/uaflix?t={HttpUtility.UrlEncode(filmUrl)}&play=true";
var tpl = new MovieTpl(title, original_title, 1);
tpl.Append(title, accsArgs(link), method: "play");
OnLog("=== RETURN: movie template ===");
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
}
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
}
}

View File

@ -1,42 +0,0 @@
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace Uaflix
{
public class ModInit
{
public static OnlinesSettings UaFlix;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
UaFlix = new OnlinesSettings("Uaflix", "https://uafix.net", streamproxy: false, useproxy: false)
{
displayname = "UaFlix",
group = 0,
group_hide = false,
globalnameproxy = null,
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "a",
password = "a",
list = new string[] { "socks5://IP:PORT" }
},
// Note: OnlinesSettings не має властивості additional, використовуємо інший підхід
};
UaFlix = ModuleInvoke.Conf("Uaflix", UaFlix).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("uaflix");
}
}
}

View File

@ -1,16 +0,0 @@
using System;
namespace Uaflix.Models
{
public class EpisodeLinkInfo
{
public string url { get; set; }
public string title { get; set; }
public int season { get; set; }
public int episode { get; set; }
// Нові поля для підтримки змішаних плеєрів
public string playerType { get; set; } // "ashdi-serial", "zetvideo-serial", "zetvideo-vod", "ashdi-vod"
public string iframeUrl { get; set; } // URL iframe для цього епізоду
}
}

View File

@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
namespace Uaflix.Models
{
/// <summary>
/// Модель для зберігання інформації про фільм
/// </summary>
public class FilmInfo
{
/// <summary>
/// URL сторінки фільму
/// </summary>
public string Url { get; set; }
/// <summary>
/// Назва фільму
/// </summary>
public string Title { get; set; }
/// <summary>
/// Рік випуску
/// </summary>
public int Year { get; set; }
/// <summary>
/// Опис фільму
/// </summary>
public string Description { get; set; }
/// <summary>
/// Постер фільму
/// </summary>
public string PosterUrl { get; set; }
/// <summary>
/// Список акторів
/// </summary>
public List<string> Actors { get; set; } = new List<string>();
/// <summary>
/// Режисер
/// </summary>
public string Director { get; set; }
/// <summary>
/// Тривалість у секундах
/// </summary>
public int Duration { get; set; }
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
namespace Uaflix.Models
{
public class PaginationInfo
{
// Словник сезонів, де ключ - номер сезону, значення - кількість сторінок
public Dictionary<int, int> Seasons { get; set; } = new Dictionary<int, int>();
// Загальна кількість сторінок (якщо потрібно)
public int TotalPages { get; set; }
// URL сторінки серіалу (базовий URL для пагінації)
public string SerialUrl { get; set; }
public List<EpisodeLinkInfo> Episodes { get; set; } = new List<EpisodeLinkInfo>();
}
}

View File

@ -1,12 +0,0 @@
using System.Collections.Generic;
using Shared.Models.Templates;
namespace Uaflix.Models
{
public class PlayResult
{
public string ashdi_url { get; set; }
public List<(string link, string quality)> streams { get; set; }
public SubtitleTpl? subtitles { get; set; }
}
}

View File

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
namespace Uaflix.Models
{
public class SearchResult
{
public string Title { get; set; }
public string Url { get; set; }
public int Year { get; set; }
public string PosterUrl { get; set; }
}
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
namespace Uaflix.Models
{
/// <summary>
/// Агрегована структура серіалу з озвучками з усіх джерел (ashdi, zetvideo-serial, zetvideo-vod, ashdi-vod)
/// </summary>
public class SerialAggregatedStructure
{
/// <summary>
/// URL головної сторінки серіалу
/// </summary>
public string SerialUrl { get; set; }
/// <summary>
/// Словник озвучок: ключ - displayName озвучки (наприклад, "[Ashdi] DniproFilm"), значення - VoiceInfo
/// </summary>
public Dictionary<string, VoiceInfo> Voices { get; set; }
/// <summary>
/// Список всіх епізодів серіалу (використовується для zetvideo-vod)
/// </summary>
public List<EpisodeLinkInfo> AllEpisodes { get; set; }
public SerialAggregatedStructure()
{
Voices = new Dictionary<string, VoiceInfo>();
AllEpisodes = new List<EpisodeLinkInfo>();
}
}
}

View File

@ -1,71 +0,0 @@
using System.Collections.Generic;
namespace Uaflix.Models
{
/// <summary>
/// Модель для зберігання інформації про озвучку серіалу
/// </summary>
public class VoiceInfo
{
/// <summary>
/// Назва озвучки без префіксу (наприклад, "DniproFilm")
/// </summary>
public string Name { get; set; }
/// <summary>
/// Тип плеєра: "ashdi-serial", "zetvideo-serial", "zetvideo-vod", "ashdi-vod"
/// </summary>
public string PlayerType { get; set; }
/// <summary>
/// Назва для відображення з префіксом плеєра (наприклад, "[Ashdi] DniproFilm")
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// Словник сезонів: ключ - номер сезону, значення - список епізодів
/// </summary>
public Dictionary<int, List<EpisodeInfo>> Seasons { get; set; }
public VoiceInfo()
{
Seasons = new Dictionary<int, List<EpisodeInfo>>();
}
}
/// <summary>
/// Модель для зберігання інформації про окремий епізод
/// </summary>
public class EpisodeInfo
{
/// <summary>
/// Номер епізоду
/// </summary>
public int Number { get; set; }
/// <summary>
/// Назва епізоду
/// </summary>
public string Title { get; set; }
/// <summary>
/// Пряме посилання на відео файл (m3u8)
/// </summary>
public string File { get; set; }
/// <summary>
/// ID епізоду у плеєрі
/// </summary>
public string Id { get; set; }
/// <summary>
/// URL постера епізоду
/// </summary>
public string Poster { get; set; }
/// <summary>
/// Субтитри у форматі Playerjs
/// </summary>
public string Subtitle { get; set; }
}
}

View File

@ -1,25 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace Uaflix
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.UaFlix;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/uaflix";
online.Add((init.displayname, url, "uaflix", init.displayindex));
}
return online;
}
}
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "Uaflix.ModInit",
"online": "Uaflix.OnlineApi"
}

View File

@ -1,142 +0,0 @@
using Shared.Engine;
using System;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Web;
using Newtonsoft.Json.Linq;
using Shared.Models.Templates;
using Shared.Models.Online.Settings;
using Shared;
namespace Unimay.Controllers
{
public class Controller : BaseOnlineController
{
ProxyManager proxyManager;
public Controller()
{
proxyManager = new ProxyManager(ModInit.Unimay);
}
[HttpGet]
[Route("unimay")]
async public ValueTask<ActionResult> Index(string title, string original_title, string code, int serial = -1, int s = -1, int e = -1, bool play = false, bool rjson = false)
{
var init = await loadKit(ModInit.Unimay);
if (await IsBadInitialization(init, rch: false))
return badInitMsg;
var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager);
if (!string.IsNullOrEmpty(code))
{
// Fetch release details
return await Release(invoke, init, code, title, original_title, serial, s, e, play, rjson);
}
else
{
// Search
return await Search(invoke, init, title, original_title, serial, rjson);
}
}
async ValueTask<ActionResult> Search(UnimayInvoke invoke, OnlinesSettings init, string title, string original_title, int serial, bool rjson)
{
string memKey = $"unimay:search:{title}:{original_title}:{serial}";
return await InvkSemaphore(init, memKey, async () =>
{
var searchResults = await invoke.Search(title, original_title, serial);
if (searchResults == null || searchResults.Content.Count == 0)
return OnError("no results");
var stpl = new SimilarTpl(searchResults.Content.Count);
var results = invoke.GetSearchResults(host, searchResults, title, original_title, serial);
foreach (var (itemTitle, itemYear, itemType, releaseUrl) in results)
{
stpl.Append(itemTitle, itemYear, itemType, releaseUrl);
}
return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml());
});
}
async ValueTask<ActionResult> Release(UnimayInvoke invoke, OnlinesSettings init, string code, string title, string original_title, int serial, int s, int e, bool play, bool rjson)
{
string memKey = $"unimay:release:{code}";
return await InvkSemaphore(init, memKey, async () =>
{
var releaseDetail = await invoke.Release(code);
if (releaseDetail == null)
return OnError("no release detail");
string itemType = releaseDetail.Type;
var playlist = releaseDetail.Playlist;
if (playlist == null || playlist.Count == 0)
return OnError("no playlist");
if (play)
{
// Get specific episode
Unimay.Models.Episode episode = null;
if (itemType == "Телесеріал")
{
if (s <= 0 || e <= 0) return OnError("invalid episode");
episode = playlist.FirstOrDefault(ep => ep.Number == e);
}
else // Movie
{
episode = playlist[0];
}
if (episode == null)
return OnError("episode not found");
string masterUrl = invoke.GetStreamUrl(episode);
if (string.IsNullOrEmpty(masterUrl))
return OnError("no stream");
return Redirect(HostStreamProxy(init, accsArgs(masterUrl), proxy: proxyManager.Get()));
}
if (itemType == "Фільм")
{
var (movieTitle, movieLink) = invoke.GetMovieResult(host, releaseDetail, title, original_title);
var mtpl = new MovieTpl(title, original_title, 1);
mtpl.Append(movieTitle, accsArgs(movieLink), method: "play");
return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml());
}
else if (itemType == "Телесеріал")
{
if (s == -1)
{
// Assume single season
var (seasonName, seasonUrl, seasonId) = invoke.GetSeasonInfo(host, code, title, original_title);
var stpl = new SeasonTpl();
stpl.Append(seasonName, seasonUrl, seasonId);
return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml());
}
else
{
// Episodes for season 1
var episodes = invoke.GetEpisodesForSeason(host, releaseDetail, title, original_title);
var mtpl = new MovieTpl(title, original_title, episodes.Count);
foreach (var (epTitle, epLink) in episodes)
{
mtpl.Append(epTitle, accsArgs(epLink), method: "play");
}
return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml());
}
}
return OnError("unsupported type");
});
}
}
}

View File

@ -1,43 +0,0 @@
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models.Module;
using Newtonsoft.Json;
using Shared;
using Shared.Engine;
using Newtonsoft.Json.Linq;
namespace Unimay
{
public class ModInit
{
public static OnlinesSettings Unimay;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded(InitspaceModel initspace)
{
Unimay = new OnlinesSettings("Unimay", "https://api.unimay.media/v1", streamproxy: false, useproxy: false)
{
displayname = "Unimay",
displayindex = 0,
proxy = new Shared.Models.Base.ProxySettings()
{
useAuth = true,
username = "a",
password = "a",
list = new string[] { "socks5://IP:PORT" }
}
};
Unimay = ModuleInvoke.Conf("Unimay", Unimay).ToObject<OnlinesSettings>();
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("unimay");
}
}
}

View File

@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
namespace Unimay.Models
{
public class Episode
{
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("hls")]
public Hls Hls { get; set; }
}
public class Hls
{
[JsonPropertyName("master")]
public string Master { get; set; }
}
}

View File

@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Unimay.Models
{
public class ReleaseResponse
{
[JsonPropertyName("code")]
public string Code { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public string Year { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } // "Фільм" або "Телесеріал"
[JsonPropertyName("playlist")]
public List<Episode> Playlist { get; set; }
}
}

View File

@ -1,41 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Unimay.Models
{
public class SearchResponse
{
[JsonPropertyName("content")]
public List<ReleaseInfo> Content { get; set; }
[JsonPropertyName("totalElements")]
public int TotalElements { get; set; }
}
public class ReleaseInfo
{
[JsonPropertyName("code")]
public string Code { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("year")]
public string Year { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } // "Фільм" або "Телесеріал"
[JsonPropertyName("names")]
public Names Names { get; set; }
}
public class Names
{
[JsonPropertyName("ukr")]
public string Ukr { get; set; }
[JsonPropertyName("eng")]
public string Eng { get; set; }
}
}

View File

@ -1,35 +0,0 @@
using Shared.Models.Base;
using System.Collections.Generic;
namespace Unimay
{
public class OnlineApi
{
public static List<(string name, string url, string plugin, int index)> Events(string host, long id, string imdb_id, long kinopoisk_id, string title, string original_title, string original_language, int year, string source, int serial, string account_email)
{
var online = new List<(string name, string url, string plugin, int index)>();
var init = ModInit.Unimay;
// Визначення isAnime згідно стандарту Lampac (Deepwiki):
// isanime = true якщо original_language == "ja" або "zh"
bool hasLang = !string.IsNullOrEmpty(original_language);
bool isanime = hasLang && (original_language == "ja" || original_language == "zh");
// Unimay — аніме-провайдер. Додаємо якщо:
// - загальний пошук (serial == -1), або
// - контент є аніме (isanime), або
// - мова невідома (немає original_language)
if (init.enable && !init.rip && (serial == -1 || isanime || !hasLang))
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/unimay";
online.Add((init.displayname, url, "unimay", init.displayindex));
}
return online;
}
}
}

View File

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -1,202 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Shared;
using Shared.Models.Online.Settings;
using Shared.Models;
using System.Linq;
using Unimay.Models;
using Shared.Engine;
using System.Net;
using System.Text;
namespace Unimay
{
public class UnimayInvoke
{
private static readonly HashSet<string> NotAllowedHosts =
new HashSet<string>(
new[]
{
"c3ZpdGFubW92aWU=",
"cG9ydGFsLXR2",
}
.Select(base64 => Encoding.UTF8.GetString(Convert.FromBase64String(base64))),
StringComparer.OrdinalIgnoreCase
);
private OnlinesSettings _init;
private ProxyManager _proxyManager;
private HybridCache _hybridCache;
private Action<string> _onLog;
public UnimayInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<SearchResponse> Search(string title, string original_title, int serial)
{
string memKey = $"unimay:search:{title}:{original_title}:{serial}";
if (_hybridCache.TryGetValue(memKey, out SearchResponse searchResults))
return searchResults;
try
{
string searchQuery = System.Web.HttpUtility.UrlEncode(title ?? original_title ?? "");
string searchUrl = $"{_init.host}/release/search?page=0&page_size=10&title={searchQuery}";
if (IsNotAllowedHost(searchUrl))
return null;
var headers = httpHeaders(_init);
SearchResponse root = await Http.Get<SearchResponse>(searchUrl, timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers);
if (root == null || root.Content == null || root.Content.Count == 0)
{
// Refresh proxy on failure
_proxyManager.Refresh();
return null;
}
_hybridCache.Set(memKey, root, cacheTime(30, init: _init));
return root;
}
catch (Exception ex)
{
_onLog($"Unimay search error: {ex.Message}");
return null;
}
}
public async Task<ReleaseResponse> Release(string code)
{
string memKey = $"unimay:release:{code}";
if (_hybridCache.TryGetValue(memKey, out ReleaseResponse releaseDetail))
return releaseDetail;
try
{
string releaseUrl = $"{_init.host}/release?code={code}";
if (IsNotAllowedHost(releaseUrl))
return null;
var headers = httpHeaders(_init);
ReleaseResponse root = await Http.Get<ReleaseResponse>(releaseUrl, timeoutSeconds: 8, proxy: _proxyManager.Get(), headers: headers);
if (root == null)
{
// Refresh proxy on failure
_proxyManager.Refresh();
return null;
}
_hybridCache.Set(memKey, root, cacheTime(60, init: _init));
return root;
}
catch (Exception ex)
{
_onLog($"Unimay release error: {ex.Message}");
return null;
}
}
public List<(string title, string year, string type, string url)> GetSearchResults(string host, SearchResponse searchResults, string title, string original_title, int serial)
{
var results = new List<(string title, string year, string type, string url)>();
foreach (var item in searchResults.Content)
{
// Filter by serial if specified (0: movie "Фільм", 1: serial "Телесеріал")
if (serial != -1)
{
bool isMovie = item.Type == "Фільм";
if ((serial == 0 && !isMovie) || (serial == 1 && isMovie))
continue;
}
string itemTitle = item.Names?.Ukr ?? item.Names?.Eng ?? item.Title;
string releaseUrl = $"{host}/unimay?code={item.Code}&title={System.Web.HttpUtility.UrlEncode(itemTitle)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}";
results.Add((itemTitle, item.Year, item.Type, releaseUrl));
}
return results;
}
public (string title, string link) GetMovieResult(string host, ReleaseResponse releaseDetail, string title, string original_title)
{
if (releaseDetail.Playlist == null || releaseDetail.Playlist.Count == 0)
return (null, null);
var movieEpisode = releaseDetail.Playlist[0];
string movieLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=0&play=true";
string movieTitle = movieEpisode.Title ?? title;
return (movieTitle, movieLink);
}
public (string seasonName, string seasonUrl, string seasonId) GetSeasonInfo(string host, string code, string title, string original_title)
{
string seasonUrl = $"{host}/unimay?code={code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1";
return ("Сезон 1", seasonUrl, "1");
}
public List<(string episodeTitle, string episodeUrl)> GetEpisodesForSeason(string host, ReleaseResponse releaseDetail, string title, string original_title)
{
var episodes = new List<(string episodeTitle, string episodeUrl)>();
if (releaseDetail.Playlist == null)
return episodes;
foreach (var ep in releaseDetail.Playlist.Where(ep => ep.Number >= 1 && ep.Number <= 24).OrderBy(ep => ep.Number))
{
string epTitle = ep.Title ?? $"Епізод {ep.Number}";
string epLink = $"{host}/unimay?code={releaseDetail.Code}&title={System.Web.HttpUtility.UrlEncode(title)}&original_title={System.Web.HttpUtility.UrlEncode(original_title ?? "")}&serial=1&s=1&e={ep.Number}&play=true";
episodes.Add((epTitle, epLink));
}
return episodes;
}
public string GetStreamUrl(Episode episode)
{
return episode.Hls?.Master;
}
private List<HeadersModel> httpHeaders(OnlinesSettings init)
{
return new List<HeadersModel>()
{
new HeadersModel("User-Agent", "Mozilla/5.0"),
new HeadersModel("Referer", init.host),
new HeadersModel("Accept", "application/json")
};
}
private static bool IsNotAllowedHost(string url)
{
if (string.IsNullOrEmpty(url))
return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
return NotAllowedHosts.Contains(uri.Host);
}
public static TimeSpan cacheTime(int multiaccess, int home = 5, int mikrotik = 2, OnlinesSettings init = null, int rhub = -1)
{
if (init != null && init.rhub && rhub != -1)
return TimeSpan.FromMinutes(rhub);
int ctime = AppInit.conf.mikrotik ? mikrotik : AppInit.conf.multiaccess ? init != null && init.cache_time > 0 ? init.cache_time : multiaccess : home;
if (ctime > multiaccess)
ctime = multiaccess;
return TimeSpan.FromMinutes(ctime);
}
}
}

View File

@ -1,6 +0,0 @@
{
"enable": true,
"version": 2,
"initspace": "Unimay.ModInit",
"online": "Unimay.OnlineApi"
}