This commit is contained in:
Felix 2025-09-20 11:47:28 +03:00
parent 3e1cab4531
commit 7a211c838b
29 changed files with 1266 additions and 153 deletions

148
AnimeON/AnimeONInvoke.cs Normal file
View File

@ -0,0 +1,148 @@
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 AnimeON.Models;
using Shared.Engine;
namespace AnimeON
{
public class AnimeONInvoke
{
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)}";
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/fundubs/{animeId}";
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);
return fundubsResponse?.FunDubs;
}
public async Task<EpisodeModel> GetEpisodes(int animeId, int playerId, int fundubId)
{
string episodesUrl = $"{_init.host}/api/player/episodes/{animeId}?take=100&skip=-1&playerId={playerId}&fundubId={fundubId}";
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/")
};
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 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

@ -33,14 +33,16 @@ namespace AnimeON.Controllers
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var seasons = await search(init, imdb_id, kinopoisk_id, title, original_title, year); var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
var seasons = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year);
if (seasons == null || seasons.Count == 0) if (seasons == null || seasons.Count == 0)
return Content("AnimeON", "text/html; charset=utf-8"); return Content("AnimeON", "text/html; charset=utf-8");
var allOptions = new List<(SearchModel season, FundubModel fundub, Player player)>(); var allOptions = new List<(SearchModel season, FundubModel fundub, Player player)>();
foreach (var season in seasons) foreach (var season in seasons)
{ {
var fundubs = await GetFundubs(init, season.Id); var fundubs = await invoke.GetFundubs(season.Id);
if (fundubs != null) if (fundubs != null)
{ {
foreach (var fundub in fundubs) foreach (var fundub in fundubs)
@ -76,7 +78,7 @@ namespace AnimeON.Controllers
return Content("AnimeON", "text/html; charset=utf-8"); return Content("AnimeON", "text/html; charset=utf-8");
var selected = allOptions[s]; var selected = allOptions[s];
var episodesData = await GetEpisodes(init, selected.season.Id, selected.player.Id, selected.fundub.Fundub.Id); var episodesData = await invoke.GetEpisodes(selected.season.Id, selected.player.Id, selected.fundub.Fundub.Id);
if (episodesData == null || episodesData.Episodes == null) if (episodesData == null || episodesData.Episodes == null)
return Content("AnimeON", "text/html; charset=utf-8"); return Content("AnimeON", "text/html; charset=utf-8");
@ -85,9 +87,31 @@ namespace AnimeON.Controllers
{ {
var streamquality = new StreamQualityTpl(); var streamquality = new StreamQualityTpl();
string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl; string streamLink = !string.IsNullOrEmpty(ep.Hls) ? ep.Hls : ep.VideoUrl;
streamquality.Append(HostStreamProxy(init, streamLink), "hls");
movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamquality.Firts().link, streamquality: streamquality); if (selected.player.Name.ToLower() == "moon" && !string.IsNullOrEmpty(streamLink) && streamLink.Contains("moonanime.art/iframe/"))
{
streamLink = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
streamquality.Append(streamLink, "hls");
movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamLink, streamquality: streamquality);
}
else if (!string.IsNullOrEmpty(streamLink))
{
streamquality.Append(HostStreamProxy(init, streamLink), "hls");
movie_tpl.Append(string.IsNullOrEmpty(ep.Name) ? $"Серія {ep.EpisodeNum}" : ep.Name, streamquality.Firts().link, streamquality: streamquality);
}
} }
if (!string.IsNullOrEmpty(episodesData.AnotherPlayer) && episodesData.AnotherPlayer.Contains("ashdi.vip"))
{
var match = Regex.Match(episodesData.AnotherPlayer, "/serial/([0-9]+)");
if (match.Success)
{
string ashdi_kp = match.Groups[1].Value;
string ashdi_link = $"/ashdi?kinopoisk_id={ashdi_kp}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}";
movie_tpl.Append("Плеєр Ashdi", ashdi_link);
}
}
return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8"); return rjson ? Content(movie_tpl.ToJson(), "application/json; charset=utf-8") : Content(movie_tpl.ToHtml(), "text/html; charset=utf-8");
} }
} }
@ -96,16 +120,26 @@ namespace AnimeON.Controllers
var tpl = new MovieTpl(title, original_title, allOptions.Count); var tpl = new MovieTpl(title, original_title, allOptions.Count);
foreach (var item in allOptions) foreach (var item in allOptions)
{ {
var episodesData = await GetEpisodes(init, item.season.Id, item.player.Id, item.fundub.Fundub.Id); var episodesData = await invoke.GetEpisodes(item.season.Id, item.player.Id, item.fundub.Fundub.Id);
if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0) if (episodesData == null || episodesData.Episodes == null || episodesData.Episodes.Count == 0)
continue; continue;
string translationName = $"[{item.player.Name}] {item.fundub.Fundub.Name}"; string translationName = $"[{item.player.Name}] {item.fundub.Fundub.Name}";
var streamquality = new StreamQualityTpl(); var streamquality = new StreamQualityTpl();
var firstEp = episodesData.Episodes.First(); var firstEp = episodesData.Episodes.FirstOrDefault();
string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl; string streamLink = !string.IsNullOrEmpty(firstEp.Hls) ? firstEp.Hls : firstEp.VideoUrl;
streamquality.Append(HostStreamProxy(init, streamLink), "hls");
tpl.Append(translationName, streamquality.Firts().link, streamquality: streamquality); if (item.player.Name.ToLower() == "moon" && !string.IsNullOrEmpty(streamLink) && streamLink.Contains("moonanime.art/iframe/"))
{
streamLink = $"{host}/animeon/play?url={HttpUtility.UrlEncode(streamLink)}";
streamquality.Append(streamLink, "hls");
tpl.Append(translationName, streamLink, streamquality: streamquality);
}
else if (!string.IsNullOrEmpty(streamLink))
{
streamquality.Append(HostStreamProxy(init, streamLink), "hls");
tpl.Append(translationName, streamquality.Firts().link, streamquality: streamquality);
}
} }
return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8"); return rjson ? Content(tpl.ToJson(), "application/json; charset=utf-8") : Content(tpl.ToHtml(), "text/html; charset=utf-8");
} }
@ -188,5 +222,24 @@ namespace AnimeON.Controllers
return null; return null;
} }
[HttpGet("animeon/play")]
public async Task<ActionResult> Play(string url)
{
if (string.IsNullOrEmpty(url))
return OnError("url is empty");
var init = await loadKit(ModInit.AnimeON);
if (!init.enable)
return Forbid();
var invoke = new AnimeONInvoke(init, hybridCache, OnLog, proxyManager);
string streamLink = await invoke.ParseMoonAnimePage(url);
if (string.IsNullOrEmpty(streamLink))
return Content("Не вдалося отримати посилання на відео", "text/html; charset=utf-8");
return Redirect(HostStreamProxy(init, streamLink));
}
} }
} }

View File

@ -1,5 +1,6 @@
using Shared; using Shared;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace AnimeON namespace AnimeON
{ {
@ -10,7 +11,7 @@ namespace AnimeON
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </summary> /// </summary>
public static void loaded() public static void loaded(InitspaceModel initspace)
{ {
AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false) AnimeON = new OnlinesSettings("AnimeON", "https://animeon.club", streamproxy: false)
{ {

View File

@ -0,0 +1,23 @@
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

@ -70,6 +70,9 @@ namespace AnimeON.Models
{ {
[JsonPropertyName("episodes")] [JsonPropertyName("episodes")]
public List<Episode> Episodes { get; set; } public List<Episode> Episodes { get; set; }
[JsonPropertyName("anotherPlayer")]
public string AnotherPlayer { get; set; }
} }
public class Episode public class Episode

29
AnimeON/Models/Serial.cs Normal file
View File

@ -0,0 +1,29 @@
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; }
}
}

26
AnimeON/Models/Voice.cs Normal file
View File

@ -0,0 +1,26 @@
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,6 +1,6 @@
{ {
"enable": true, "enable": true,
"version": 1, "version": 2,
"initspace": "AnimeON.ModInit", "initspace": "AnimeON.ModInit",
"online": "AnimeON.OnlineApi" "online": "AnimeON.OnlineApi"
} }

View File

@ -0,0 +1,312 @@
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 HtmlAgilityPack;
using CikavaIdeya.Models;
using Shared.Engine;
using System.Linq;
namespace CikavaIdeya
{
public class CikavaIdeyaInvoke
{
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) };
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;
// Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди)
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"))
{
result.iframe_url = 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 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

@ -32,7 +32,9 @@ namespace CikavaIdeya.Controllers
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var episodesInfo = await search(init, imdb_id, kinopoisk_id, title, original_title, year, serial == 0); 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) if (episodesInfo == null)
return Content("CikavaIdeya", "text/html; charset=utf-8"); return Content("CikavaIdeya", "text/html; charset=utf-8");
@ -45,7 +47,7 @@ namespace CikavaIdeya.Controllers
if (episode == null) if (episode == null)
return Content("CikavaIdeya", "text/html; charset=utf-8"); return Content("CikavaIdeya", "text/html; charset=utf-8");
var playResult = await ParseEpisode(init, episode.url); var playResult = await invoke.ParseEpisode(episode.url);
if (!string.IsNullOrEmpty(playResult.iframe_url)) if (!string.IsNullOrEmpty(playResult.iframe_url))
{ {
@ -182,7 +184,7 @@ namespace CikavaIdeya.Controllers
if (filmUrl == null) if (filmUrl == null)
{ {
var firstNode = filmNodes.First().SelectSingleNode(".//a[@class='th-in']"); var firstNode = filmNodes.FirstOrDefault()?.SelectSingleNode(".//a[@class='th-in']");
if (firstNode != null) if (firstNode != null)
filmUrl = firstNode.GetAttributeValue("href", ""); filmUrl = firstNode.GetAttributeValue("href", "");
} }

View File

@ -1,5 +1,6 @@
using Shared; using Shared;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace CikavaIdeya namespace CikavaIdeya
{ {
@ -10,7 +11,7 @@ namespace CikavaIdeya
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </summary> /// </summary>
public static void loaded() public static void loaded(InitspaceModel initspace)
{ {
CikavaIdeya = new OnlinesSettings("CikavaIdeya", "https://cikava-ideya.top", streamproxy: false) CikavaIdeya = new OnlinesSettings("CikavaIdeya", "https://cikava-ideya.top", streamproxy: false)
{ {

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,14 @@
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,6 +1,6 @@
{ {
"enable": true, "enable": true,
"version": 1, "version": 2,
"initspace": "CikavaIdeya.ModInit", "initspace": "CikavaIdeya.ModInit",
"online": "CikavaIdeya.OnlineApi" "online": "CikavaIdeya.OnlineApi"
} }

View File

@ -11,25 +11,10 @@ using Shared.Models.Templates;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models; using Shared.Models;
using Uaflix.Models;
namespace Uaflix.Controllers namespace Uaflix.Controllers
{ {
#region 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 ashdi_url { get; set; }
public List<(string link, string quality)> streams { get; set; }
public SubtitleTpl? subtitles { get; set; }
}
#endregion
public class Controller : BaseOnlineController public class Controller : BaseOnlineController
{ {
@ -48,7 +33,9 @@ namespace Uaflix.Controllers
if (!init.enable) if (!init.enable)
return Forbid(); return Forbid();
var episodesInfo = await search(init, imdb_id, kinopoisk_id, title, original_title, year, serial == 0); var invoke = new UaflixInvoke(init, hybridCache, OnLog, proxyManager);
var episodesInfo = await invoke.Search(imdb_id, kinopoisk_id, title, original_title, year, serial == 0);
if (episodesInfo == null) if (episodesInfo == null)
return Content("Uaflix", "text/html; charset=utf-8"); return Content("Uaflix", "text/html; charset=utf-8");
@ -61,7 +48,7 @@ namespace Uaflix.Controllers
if (episode == null) if (episode == null)
return Content("Uaflix", "text/html; charset=utf-8"); return Content("Uaflix", "text/html; charset=utf-8");
var playResult = await ParseEpisode(init, episode.url); var playResult = await invoke.ParseEpisode(episode.url);
if (!string.IsNullOrEmpty(playResult.ashdi_url)) if (!string.IsNullOrEmpty(playResult.ashdi_url))
{ {
@ -109,10 +96,10 @@ namespace Uaflix.Controllers
} }
} }
async ValueTask<List<EpisodeLinkInfo>> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false) async ValueTask<List<Uaflix.Models.EpisodeLinkInfo>> search(OnlinesSettings init, string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false)
{ {
string memKey = $"UaFlix:search:{kinopoisk_id}:{imdb_id}"; string memKey = $"UaFlix:search:{kinopoisk_id}:{imdb_id}";
if (hybridCache.TryGetValue(memKey, out List<EpisodeLinkInfo> res)) if (hybridCache.TryGetValue(memKey, out List<Uaflix.Models.EpisodeLinkInfo> res))
return res; return res;
try try
@ -143,14 +130,14 @@ namespace Uaflix.Controllers
} }
if (filmUrl == null) if (filmUrl == null)
filmUrl = filmNodes.First().GetAttributeValue("href", ""); filmUrl = filmNodes.FirstOrDefault()?.GetAttributeValue("href", "");
if (!filmUrl.StartsWith("http")) if (!filmUrl.StartsWith("http"))
filmUrl = init.host + filmUrl; filmUrl = init.host + filmUrl;
if (isfilm) if (isfilm)
{ {
res = new List<EpisodeLinkInfo>() { new EpisodeLinkInfo() { url = filmUrl } }; res = new List<Uaflix.Models.EpisodeLinkInfo>() { new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl } };
hybridCache.Set(memKey, res, cacheTime(20)); hybridCache.Set(memKey, res, cacheTime(20));
return res; return res;
} }
@ -158,11 +145,11 @@ namespace Uaflix.Controllers
var filmHtml = await Http.Get(filmUrl, headers: headers); var filmHtml = await Http.Get(filmUrl, headers: headers);
doc.LoadHtml(filmHtml); doc.LoadHtml(filmHtml);
res = new List<EpisodeLinkInfo>(); res = new List<Uaflix.Models.EpisodeLinkInfo>();
var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels2')]//a[contains(@class, 'vi-img')]"); var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels2')]//a[contains(@class, 'vi-img')]");
if (episodeNodes != null) if (episodeNodes != null)
{ {
foreach (var episodeNode in episodeNodes.Reverse()) foreach (var episodeNode in episodeNodes.Reverse().ToList())
{ {
string episodeUrl = episodeNode.GetAttributeValue("href", ""); string episodeUrl = episodeNode.GetAttributeValue("href", "");
if (!episodeUrl.StartsWith("http")) if (!episodeUrl.StartsWith("http"))
@ -171,7 +158,7 @@ namespace Uaflix.Controllers
var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)"); var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)");
if (match.Success) if (match.Success)
{ {
res.Add(new EpisodeLinkInfo res.Add(new Uaflix.Models.EpisodeLinkInfo
{ {
url = episodeUrl, url = episodeUrl,
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}", title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}",
@ -187,7 +174,7 @@ namespace Uaflix.Controllers
var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe[contains(@src, 'ashdi.vip/serial/')]"); var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe[contains(@src, 'ashdi.vip/serial/')]");
if (iframe != null) if (iframe != null)
{ {
res.Add(new EpisodeLinkInfo() { url = filmUrl, season = 1, episode = 1 }); res.Add(new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl, season = 1, episode = 1 });
} }
} }
@ -203,9 +190,9 @@ namespace Uaflix.Controllers
return null; return null;
} }
async Task<PlayResult> ParseEpisode(OnlinesSettings init, string url) async Task<Uaflix.Models.PlayResult> ParseEpisode(OnlinesSettings init, string url)
{ {
var result = new PlayResult() { streams = new List<(string, string)>() }; var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() };
try try
{ {
string html = await Http.Get(url, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) }); string html = await Http.Get(url, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", init.host) });
@ -305,7 +292,7 @@ namespace Uaflix.Controllers
if (!string.IsNullOrEmpty(subtitle)) if (!string.IsNullOrEmpty(subtitle))
{ {
var match = new Regex("\\[([^\\]]+)\\](https?://[^\\,]+)").Match(subtitle); var match = new Regex("\\[([^\\]]+)\\](https?://[^\\,]+)").Match(subtitle);
var st = new SubtitleTpl(); var st = new Shared.Models.Templates.SubtitleTpl();
while (match.Success) while (match.Success)
{ {
st.Append(match.Groups[1].Value, match.Groups[2].Value); st.Append(match.Groups[1].Value, match.Groups[2].Value);

View File

@ -1,5 +1,6 @@
using Shared; using Shared;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace Uaflix namespace Uaflix
{ {
@ -10,7 +11,7 @@ namespace Uaflix
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </summary> /// </summary>
public static void loaded() public static void loaded(InitspaceModel initspace)
{ {
UaFlix = new OnlinesSettings("Uaflix", "https://uafix.net", streamproxy: false) UaFlix = new OnlinesSettings("Uaflix", "https://uafix.net", streamproxy: false)
{ {

View File

@ -0,0 +1,12 @@
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; }
}
}

View File

@ -0,0 +1,12 @@
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; }
}
}

250
Uaflix/UaflixInvoke.cs Normal file
View File

@ -0,0 +1,250 @@
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 HtmlAgilityPack;
using Uaflix.Controllers;
using Shared.Engine;
using Uaflix.Models;
using System.Linq;
using Shared.Models.Templates;
namespace Uaflix
{
public class UaflixInvoke
{
private OnlinesSettings _init;
private HybridCache _hybridCache;
private Action<string> _onLog;
private ProxyManager _proxyManager;
public UaflixInvoke(OnlinesSettings init, HybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
{
_init = init;
_hybridCache = hybridCache;
_onLog = onLog;
_proxyManager = proxyManager;
}
public async Task<List<Uaflix.Models.EpisodeLinkInfo>> Search(string imdb_id, long kinopoisk_id, string title, string original_title, int year, bool isfilm = false)
{
string memKey = $"UaFlix:search:{kinopoisk_id}:{imdb_id}";
if (_hybridCache.TryGetValue(memKey, out List<Uaflix.Models.EpisodeLinkInfo> res))
return res;
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) };
var searchHtml = await Http.Get(searchUrl, headers: headers, proxy: _proxyManager.Get());
var doc = new HtmlDocument();
doc.LoadHtml(searchHtml);
var filmNodes = doc.DocumentNode.SelectNodes("//a[contains(@class, 'sres-wrap')]");
if (filmNodes == null) return null;
string filmUrl = null;
foreach (var filmNode in filmNodes)
{
var h2Node = filmNode.SelectSingleNode(".//h2");
if (h2Node == null || !h2Node.InnerText.Trim().ToLower().Contains(filmTitle.ToLower())) continue;
var descNode = filmNode.SelectSingleNode(".//div[contains(@class, 'sres-desc')]");
if (year > 0 && (descNode?.InnerText ?? "").Contains(year.ToString()))
{
filmUrl = filmNode.GetAttributeValue("href", "");
break;
}
}
if (filmUrl == null)
filmUrl = filmNodes.FirstOrDefault()?.GetAttributeValue("href", "");
if (!filmUrl.StartsWith("http"))
filmUrl = _init.host + filmUrl;
if (isfilm)
{
res = new List<Uaflix.Models.EpisodeLinkInfo>() { new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl } };
_hybridCache.Set(memKey, res, cacheTime(20));
return res;
}
var filmHtml = await Http.Get(filmUrl, headers: headers, proxy: _proxyManager.Get());
doc.LoadHtml(filmHtml);
res = new List<Uaflix.Models.EpisodeLinkInfo>();
var episodeNodes = doc.DocumentNode.SelectNodes("//div[contains(@class, 'frels2')]//a[contains(@class, 'vi-img')]");
if (episodeNodes != null)
{
foreach (var episodeNode in episodeNodes.Reverse().ToList())
{
string episodeUrl = episodeNode.GetAttributeValue("href", "");
if (!episodeUrl.StartsWith("http"))
episodeUrl = _init.host + episodeUrl;
var match = Regex.Match(episodeUrl, @"season-(\d+).*?episode-(\d+)");
if (match.Success)
{
res.Add(new Uaflix.Models.EpisodeLinkInfo
{
url = episodeUrl,
title = episodeNode.SelectSingleNode(".//div[@class='vi-rate']")?.InnerText.Trim() ?? $"Епізод {match.Groups[2].Value}",
season = int.Parse(match.Groups[1].Value),
episode = int.Parse(match.Groups[2].Value)
});
}
}
}
if (res.Count == 0)
{
var iframe = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'video-box')]//iframe[contains(@src, 'ashdi.vip/serial/')]");
if (iframe != null)
{
res.Add(new Uaflix.Models.EpisodeLinkInfo() { url = filmUrl, season = 1, episode = 1 });
}
}
if (res.Count > 0)
_hybridCache.Set(memKey, res, cacheTime(20));
return res;
}
catch (Exception ex)
{
_onLog($"UaFlix search error: {ex.Message}");
}
return null;
}
public async Task<Uaflix.Models.PlayResult> ParseEpisode(string url)
{
var result = new Uaflix.Models.PlayResult() { streams = new List<(string, string)>() };
try
{
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[contains(@class, 'video-box')]//iframe");
if (iframe != null)
{
string iframeUrl = iframe.GetAttributeValue("src", "").Replace("&", "&");
if (iframeUrl.StartsWith("//"))
iframeUrl = "https:" + iframeUrl;
if (iframeUrl.Contains("ashdi.vip/serial/"))
{
result.ashdi_url = iframeUrl;
return result;
}
if (iframeUrl.Contains("zetvideo.net"))
result.streams = await ParseAllZetvideoSources(iframeUrl);
else if (iframeUrl.Contains("ashdi.vip"))
{
result.streams = await ParseAllAshdiSources(iframeUrl);
var idMatch = Regex.Match(iframeUrl, @"_(\d+)|vod/(\d+)");
if (idMatch.Success)
{
string ashdiId = idMatch.Groups[1].Success ? idMatch.Groups[1].Value : idMatch.Groups[2].Value;
result.subtitles = await GetAshdiSubtitles(ashdiId);
}
}
}
}
catch (Exception ex)
{
_onLog($"ParseEpisode error: {ex.Message}");
}
return result;
}
async Task<List<(string link, string quality)>> ParseAllZetvideoSources(string iframeUrl)
{
var result = new List<(string link, string quality)>();
var html = await Http.Get(iframeUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://zetvideo.net/") }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html)) return result;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var script = doc.DocumentNode.SelectSingleNode("//script[contains(text(), 'file:')]");
if (script != null)
{
var match = Regex.Match(script.InnerText, @"file:\s*""([^""]+\.m3u8)");
if (match.Success)
{
result.Add((match.Groups[1].Value, "1080p"));
return result;
}
}
var sourceNodes = doc.DocumentNode.SelectNodes("//source[contains(@src, '.m3u8')]");
if (sourceNodes != null)
{
foreach (var node in sourceNodes)
{
result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p"));
}
}
return result;
}
async Task<List<(string link, string quality)>> ParseAllAshdiSources(string iframeUrl)
{
var result = new List<(string link, string quality)>();
var html = await Http.Get(iframeUrl, headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }, proxy: _proxyManager.Get());
if (string.IsNullOrEmpty(html)) return result;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var sourceNodes = doc.DocumentNode.SelectNodes("//source[contains(@src, '.m3u8')]");
if (sourceNodes != null)
{
foreach (var node in sourceNodes)
{
result.Add((node.GetAttributeValue("src", null), node.GetAttributeValue("label", null) ?? node.GetAttributeValue("res", null) ?? "1080p"));
}
}
return result;
}
async Task<SubtitleTpl?> GetAshdiSubtitles(string id)
{
var html = await Http.Get($"https://ashdi.vip/vod/{id}", headers: new List<HeadersModel>() { new HeadersModel("User-Agent", "Mozilla/5.0"), new HeadersModel("Referer", "https://ashdi.vip/") }, proxy: _proxyManager.Get());
string subtitle = new Regex("subtitle(\")?:\"([^\"]+)\"").Match(html).Groups[2].Value;
if (!string.IsNullOrEmpty(subtitle))
{
var match = new Regex("\\[([^\\]]+)\\](https?://[^\\,]+)").Match(subtitle);
var st = new Shared.Models.Templates.SubtitleTpl();
while (match.Success)
{
st.Append(match.Groups[1].Value, match.Groups[2].Value);
match = match.NextMatch();
}
if (!st.IsEmpty())
return st;
}
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);
}
}
}

View File

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

View File

@ -29,65 +29,35 @@ namespace Unimay.Controllers
if (await IsBadInitialization(init, rch: false)) if (await IsBadInitialization(init, rch: false))
return badInitMsg; return badInitMsg;
var proxy = proxyManager.Get(); var invoke = new UnimayInvoke(init, hybridCache, OnLog, proxyManager);
if (!string.IsNullOrEmpty(code)) if (!string.IsNullOrEmpty(code))
{ {
// Fetch release details // Fetch release details
return await Release(init, proxy, code, title, original_title, serial, s, e, play, rjson); return await Release(invoke, init, code, title, original_title, serial, s, e, play, rjson);
} }
else else
{ {
// Search // Search
return await Search(init, proxy, title, original_title, serial, rjson); return await Search(invoke, init, title, original_title, serial, rjson);
} }
} }
async ValueTask<ActionResult> Search(OnlinesSettings init, System.Net.WebProxy proxy, string title, string original_title, int serial, bool 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}"; string memKey = $"unimay:search:{title}:{original_title}:{serial}";
return await InvkSemaphore(init, memKey, async () => return await InvkSemaphore(init, memKey, async () =>
{ {
if (!hybridCache.TryGetValue(memKey, out JArray searchResults)) var searchResults = await invoke.Search(title, original_title, serial);
{ if (searchResults == null || searchResults.Content.Count == 0)
string searchQuery = HttpUtility.UrlEncode(title ?? original_title ?? "");
string searchUrl = $"{init.host}/release/search?page=0&page_size=10&title={searchQuery}";
var headers = httpHeaders(init);
JObject root = await Http.Get<JObject>(searchUrl, timeoutSeconds: 8, proxy: proxy, headers: headers);
if (root == null || !root.ContainsKey("content") || ((JArray)root["content"]).Count == 0)
{
proxyManager.Refresh();
return OnError("search failed");
}
searchResults = (JArray)root["content"];
hybridCache.Set(memKey, searchResults, cacheTime(30, init: init));
}
if (searchResults == null || searchResults.Count == 0)
return OnError("no results"); return OnError("no results");
var stpl = new SimilarTpl(searchResults.Count); var stpl = new SimilarTpl(searchResults.Content.Count);
var results = invoke.GetSearchResults(host, searchResults, title, original_title, serial);
foreach (JObject item in searchResults) foreach (var (itemTitle, itemYear, itemType, releaseUrl) in results)
{ {
string itemCode = item.Value<string>("code");
string itemTitle = item["names"]?["ukr"]?.Value<string>() ?? item.Value<string>("title");
string itemYear = item.Value<string>("year");
string itemType = item.Value<string>("type"); // "Телесеріал" or "Фільм"
// Filter by serial if specified (0: movie "Фільм", 1: serial "Телесеріал")
if (serial != -1)
{
bool isMovie = itemType == "Фільм";
if ((serial == 0 && !isMovie) || (serial == 1 && isMovie))
continue;
}
string releaseUrl = $"{host}/unimay?code={itemCode}&title={HttpUtility.UrlEncode(itemTitle)}&original_title={HttpUtility.UrlEncode(original_title ?? "")}&serial={serial}";
stpl.Append(itemTitle, itemYear, itemType, releaseUrl); stpl.Append(itemTitle, itemYear, itemType, releaseUrl);
} }
@ -95,34 +65,18 @@ namespace Unimay.Controllers
}); });
} }
async ValueTask<ActionResult> Release(OnlinesSettings init, System.Net.WebProxy proxy, string code, string title, string original_title, int serial, int s, int e, bool play, bool rjson) 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}"; string memKey = $"unimay:release:{code}";
return await InvkSemaphore(init, memKey, async () => return await InvkSemaphore(init, memKey, async () =>
{ {
if (!hybridCache.TryGetValue(memKey, out JObject releaseDetail)) var releaseDetail = await invoke.Release(code);
{
string releaseUrl = $"{init.host}/release?code={code}";
var headers = httpHeaders(init);
JObject root = await Http.Get<JObject>(releaseUrl, timeoutSeconds: 8, proxy: proxy, headers: headers);
if (root == null)
{
proxyManager.Refresh();
return OnError("release failed");
}
releaseDetail = root;
hybridCache.Set(memKey, releaseDetail, cacheTime(60, init: init));
}
if (releaseDetail == null) if (releaseDetail == null)
return OnError("no release detail"); return OnError("no release detail");
string itemType = releaseDetail.Value<string>("type"); string itemType = releaseDetail.Type;
JArray playlist = (JArray)releaseDetail["playlist"]; var playlist = releaseDetail.Playlist;
if (playlist == null || playlist.Count == 0) if (playlist == null || playlist.Count == 0)
return OnError("no playlist"); return OnError("no playlist");
@ -130,33 +84,32 @@ namespace Unimay.Controllers
if (play) if (play)
{ {
// Get specific episode // Get specific episode
JObject episode = null; Unimay.Models.Episode episode = null;
if (itemType == "Телесеріал") if (itemType == "Телесеріал")
{ {
if (s <= 0 || e <= 0) return OnError("invalid episode"); if (s <= 0 || e <= 0) return OnError("invalid episode");
episode = playlist.FirstOrDefault(ep => (int?)ep["number"] == e) as JObject; episode = playlist.FirstOrDefault(ep => ep.Number == e);
} }
else // Movie else // Movie
{ {
episode = playlist[0] as JObject; episode = playlist[0];
} }
if (episode == null) if (episode == null)
return OnError("episode not found"); return OnError("episode not found");
string masterUrl = episode["hls"]?["master"]?.Value<string>(); string masterUrl = invoke.GetStreamUrl(episode);
if (string.IsNullOrEmpty(masterUrl)) if (string.IsNullOrEmpty(masterUrl))
return OnError("no stream"); return OnError("no stream");
return Redirect(HostStreamProxy(init, masterUrl, proxy: proxy)); return Redirect(HostStreamProxy(init, masterUrl, proxy: proxyManager.Get()));
} }
if (itemType == "Фільм") if (itemType == "Фільм")
{ {
JObject movieEpisode = playlist[0] as JObject; var (movieTitle, movieLink) = invoke.GetMovieResult(host, releaseDetail, title, original_title);
string movieLink = $"{host}/unimay?code={code}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=0&play=true";
var mtpl = new MovieTpl(title, original_title, 1); var mtpl = new MovieTpl(title, original_title, 1);
mtpl.Append(movieEpisode["title"]?.Value<string>() ?? title, movieLink); mtpl.Append(movieTitle, movieLink);
return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml()); return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml());
} }
else if (itemType == "Телесеріал") else if (itemType == "Телесеріал")
@ -164,27 +117,18 @@ namespace Unimay.Controllers
if (s == -1) if (s == -1)
{ {
// Assume single season // Assume single season
var (seasonName, seasonUrl, seasonId) = invoke.GetSeasonInfo(host, code, title, original_title);
var stpl = new SeasonTpl(); var stpl = new SeasonTpl();
stpl.Append("Сезон 1", $"{host}/unimay?code={code}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=1&s=1", "1"); stpl.Append(seasonName, seasonUrl, seasonId);
return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml()); return ContentTo(rjson ? stpl.ToJson() : stpl.ToHtml());
} }
else else
{ {
// Episodes for season 1 // Episodes for season 1
var episodes = new List<JObject>(); var episodes = invoke.GetEpisodesForSeason(host, releaseDetail, title, original_title);
foreach (JObject ep in playlist)
{
int epNum = (int)ep["number"];
if (epNum >= 1 && epNum <= 24) // Assume season 1
episodes.Add(ep);
}
var mtpl = new MovieTpl(title, original_title, episodes.Count); var mtpl = new MovieTpl(title, original_title, episodes.Count);
foreach (JObject ep in episodes.OrderBy(ep => (int)ep["number"])) foreach (var (epTitle, epLink) in episodes)
{ {
int epNum = (int)ep["number"];
string epTitle = ep["title"]?.Value<string>() ?? $"Епізод {epNum}";
string epLink = $"{host}/unimay?code={code}&title={HttpUtility.UrlEncode(title)}&original_title={HttpUtility.UrlEncode(original_title)}&serial=1&s=1&e={epNum}&play=true";
mtpl.Append(epTitle, epLink); mtpl.Append(epTitle, epLink);
} }
return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml()); return ContentTo(rjson ? mtpl.ToJson() : mtpl.ToHtml());

View File

@ -1,5 +1,6 @@
using Shared; using Shared;
using Shared.Models.Online.Settings; using Shared.Models.Online.Settings;
using Shared.Models.Module;
namespace Unimay namespace Unimay
{ {
@ -10,9 +11,9 @@ namespace Unimay
/// <summary> /// <summary>
/// модуль загружен /// модуль загружен
/// </summary> /// </summary>
public static void loaded() public static void loaded(InitspaceModel initspace)
{ {
Unimay = new OnlinesSettings("Unimay", "https://api.unimay.media/v1", streamproxy: true) Unimay = new OnlinesSettings("Unimay", "https://api.unimay.media/v1", streamproxy: false)
{ {
displayname = "Unimay" displayname = "Unimay"
}; };

22
Unimay/Models/Episode.cs Normal file
View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,41 @@
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

@ -9,7 +9,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Lampac.csproj" /> <Reference Include="Shared">
<HintPath>..\..\Shared.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
</Project> </Project>

173
Unimay/UnimayInvoke.cs Normal file
View File

@ -0,0 +1,173 @@
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;
namespace Unimay
{
public class UnimayInvoke
{
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}";
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}";
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")
};
}
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 +1,6 @@
{ {
"enable": true, "enable": true,
"version": 1, "version": 2,
"initspace": "Unimay.ModInit", "initspace": "Unimay.ModInit",
"online": "Unimay.OnlineApi" "online": "Unimay.OnlineApi"
} }