Add CikavaIdeya

This commit is contained in:
Felix 2025-09-14 15:19:17 +03:00
parent 8c495e9439
commit b063ba9821
6 changed files with 462 additions and 0 deletions

View File

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

373
CikavaIdeya/Controller.cs Normal file
View File

@ -0,0 +1,373 @@
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 Shared.Models.Online.Settings;
using Shared.Models;
using CikavaIdeya.Models;
namespace CikavaIdeya.Controllers
{
public class Controller : BaseOnlineController
{
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 episodesInfo = await search(init, 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");
var playResult = await ParseEpisode(init, episode.url);
if (!string.IsNullOrEmpty(playResult.iframe_url))
{
// Для CikavaIdeya ми просто повертаємо iframe URL
return Redirect(playResult.iframe_url);
}
if (playResult.streams != null && playResult.streams.Count > 0)
return Redirect(HostStreamProxy(init, 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, link);
}
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, link);
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) };
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.First().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;
// Отримуємо список епізодів (для фільмів - один епізод, для серіалів - всі епізоди)
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;
}
// Інакше парсимо сторінку
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;
}
}
}

24
CikavaIdeya/ModInit.cs Normal file
View File

@ -0,0 +1,24 @@
using Shared;
using Shared.Models.Online.Settings;
namespace CikavaIdeya
{
public class ModInit
{
public static OnlinesSettings CikavaIdeya;
/// <summary>
/// модуль загружен
/// </summary>
public static void loaded()
{
CikavaIdeya = new OnlinesSettings("CikavaIdeya", "https://cikava-ideya.top", streamproxy: false)
{
displayname = "ЦікаваІдея"
};
// Виводити "уточнити пошук"
AppInit.conf.online.with_search.Add("cikavaideya");
}
}
}

View File

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

25
CikavaIdeya/OnlineApi.cs Normal file
View File

@ -0,0 +1,25 @@
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;
if (init.enable && !init.rip)
{
string url = init.overridehost;
if (string.IsNullOrEmpty(url))
url = $"{host}/cikavaideya";
online.Add((init.displayname, url, "cikavaideya", init.displayindex > 0 ? init.displayindex : online.Count));
}
return online;
}
}
}

View File

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