mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 17:32:20 +00:00
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:
parent
b139444cba
commit
7e7c8c9659
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,9 +0,0 @@
|
|||||||
/.idea/
|
|
||||||
/AIDocumentation/
|
|
||||||
/LampaC/
|
|
||||||
/BanderaBackend/
|
|
||||||
/Kinovezha/
|
|
||||||
/.clinerules/moduls.md
|
|
||||||
/.clinerules/uaflix-optimization.md
|
|
||||||
/.clinerules/
|
|
||||||
/.qodo/
|
|
||||||
@ -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>
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "AnimeON.ModInit",
|
|
||||||
"online": "AnimeON.OnlineApi"
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "Bamboo.ModInit",
|
|
||||||
"online": "Bamboo.OnlineApi"
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "CikavaIdeya.ModInit",
|
|
||||||
"online": "CikavaIdeya.OnlineApi"
|
|
||||||
}
|
|
||||||
201
LICENSE
201
LICENSE
@ -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.
|
|
||||||
76
README.md
76
README.md
@ -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
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "StarLight.ModInit",
|
|
||||||
"online": "StarLight.OnlineApi"
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "UAKino.ModInit",
|
|
||||||
"online": "UAKino.OnlineApi"
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "UaTUT.ModInit",
|
|
||||||
"online": "UaTUT.OnlineApi"
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 для цього епізоду
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "Uaflix.ModInit",
|
|
||||||
"online": "Uaflix.OnlineApi"
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 2,
|
|
||||||
"initspace": "Unimay.ModInit",
|
|
||||||
"online": "Unimay.OnlineApi"
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user