mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-16 09:22:21 +00:00
feat: Add JackTor online module for torrent search and streaming.
This commit is contained in:
parent
ae39beb8b6
commit
208aad107e
463
JackTor/Controller.cs
Normal file
463
JackTor/Controller.cs
Normal file
@ -0,0 +1,463 @@
|
||||
using JackTor.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Online.PiTor;
|
||||
using Shared.Models.Online.Settings;
|
||||
using Shared.Models.Templates;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace JackTor.Controllers
|
||||
{
|
||||
public class Controller : BaseOnlineController<JackTorSettings>
|
||||
{
|
||||
ProxyManager proxyManager;
|
||||
|
||||
public Controller() : base(ModInit.Settings)
|
||||
{
|
||||
proxyManager = new ProxyManager(ModInit.Settings);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("jacktor")]
|
||||
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 = null,
|
||||
int s = -1,
|
||||
bool rjson = false,
|
||||
bool checksearch = false)
|
||||
{
|
||||
await UpdateService.ConnectAsync(host);
|
||||
|
||||
var init = await loadKit(ModInit.Settings);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
if (NoAccessGroup(init, out string error_msg))
|
||||
return Json(new { accsdb = true, error_msg });
|
||||
|
||||
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
|
||||
if (checksearch)
|
||||
{
|
||||
if (AppInit.conf?.online?.checkOnlineSearch != true)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
var check = await invoke.Search(title, original_title, year, serial, original_language);
|
||||
if (check.Count > 0)
|
||||
return Content("data-json=", "text/plain; charset=utf-8");
|
||||
|
||||
return OnError("jacktor", proxyManager);
|
||||
}
|
||||
|
||||
var torrents = await invoke.Search(title, original_title, year, serial, original_language);
|
||||
if (torrents == null || torrents.Count == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
if (serial == 1)
|
||||
{
|
||||
var seasons = torrents
|
||||
.Where(i => i.Seasons != null && i.Seasons.Length > 0)
|
||||
.SelectMany(i => i.Seasons)
|
||||
.Distinct()
|
||||
.OrderBy(i => i)
|
||||
.ToList();
|
||||
|
||||
string enTitle = HttpUtility.UrlEncode(title);
|
||||
string enOriginal = HttpUtility.UrlEncode(original_title);
|
||||
|
||||
if (s == -1 && seasons.Count > 0)
|
||||
{
|
||||
string quality = torrents.FirstOrDefault(i => i.Quality >= 2160)?.QualityLabel
|
||||
?? torrents.FirstOrDefault(i => i.Quality >= 1080)?.QualityLabel
|
||||
?? torrents.FirstOrDefault(i => i.Quality >= 720)?.QualityLabel
|
||||
?? "720p";
|
||||
|
||||
var seasonTpl = new SeasonTpl(quality: quality);
|
||||
foreach (int season in seasons)
|
||||
{
|
||||
seasonTpl.Append(
|
||||
$"{season} сезон",
|
||||
$"{host}/jacktor?rjson={rjson}&title={enTitle}&original_title={enOriginal}&year={year}&original_language={original_language}&serial=1&s={season}",
|
||||
season);
|
||||
}
|
||||
|
||||
return rjson
|
||||
? Content(seasonTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(seasonTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
int targetSeason = s == -1 ? 1 : s;
|
||||
var releases = torrents
|
||||
.Where(i => i.Seasons == null || i.Seasons.Length == 0 || i.Seasons.Contains(targetSeason))
|
||||
.ToList();
|
||||
|
||||
if (releases.Count == 0)
|
||||
releases = torrents;
|
||||
|
||||
var similarTpl = new SimilarTpl();
|
||||
foreach (var torrent in releases)
|
||||
{
|
||||
string releaseName = string.IsNullOrWhiteSpace(torrent.Voice)
|
||||
? (torrent.Tracker ?? "Без назви")
|
||||
: torrent.Voice;
|
||||
|
||||
string qualityInfo = $"{torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}";
|
||||
string releaseLink = accsArgs($"{host}/jacktor/serial/{torrent.Rid}?rjson={rjson}&title={enTitle}&original_title={enOriginal}&s={targetSeason}");
|
||||
|
||||
similarTpl.Append(releaseName, null, qualityInfo, releaseLink);
|
||||
}
|
||||
|
||||
return rjson
|
||||
? Content(similarTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(similarTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
else
|
||||
{
|
||||
var movieTpl = new MovieTpl(title, original_title);
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
string voice = string.IsNullOrWhiteSpace(torrent.Voice)
|
||||
? (torrent.Tracker ?? "Торрент")
|
||||
: torrent.Voice;
|
||||
|
||||
string voiceName = $"{torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}";
|
||||
string streamLink = accsArgs($"{host}/jacktor/s{torrent.Rid}");
|
||||
|
||||
movieTpl.Append(
|
||||
voice,
|
||||
streamLink,
|
||||
voice_name: voiceName,
|
||||
quality: torrent.Quality > 0 ? torrent.Quality.ToString() : null);
|
||||
}
|
||||
|
||||
return rjson
|
||||
? Content(movieTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(movieTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("jacktor/serial/{rid}")]
|
||||
async public ValueTask<ActionResult> Serial(string rid, string account_email, string title, string original_title, int s = 1, bool rjson = false)
|
||||
{
|
||||
var init = await loadKit(ModInit.Settings);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
if (NoAccessGroup(init, out string error_msg))
|
||||
return Json(new { accsdb = true, error_msg });
|
||||
|
||||
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
if (!invoke.TryGetSource(rid, out JackTorSourceCache source))
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
string memKey = $"jacktor:serial:{rid}";
|
||||
|
||||
return await InvkSemaphore(memKey, null, async () =>
|
||||
{
|
||||
if (!hybridCache.TryGetValue(memKey, out FileStat[] fileStats))
|
||||
{
|
||||
var ts = ResolveProbeTorrentServer(init, account_email);
|
||||
if (string.IsNullOrWhiteSpace(ts.host))
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
string hashResponse = await Http.Post(
|
||||
$"{ts.host}/torrents",
|
||||
BuildAddPayload(source.SourceUri),
|
||||
timeoutSeconds: 8,
|
||||
headers: ts.headers);
|
||||
|
||||
string hash = ExtractHash(hashResponse);
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
Stat stat = null;
|
||||
DateTime deadline = DateTime.Now.AddSeconds(20);
|
||||
|
||||
while (true)
|
||||
{
|
||||
stat = await Http.Post<Stat>(
|
||||
$"{ts.host}/torrents",
|
||||
BuildGetPayload(hash),
|
||||
timeoutSeconds: 3,
|
||||
headers: ts.headers);
|
||||
|
||||
if (stat?.file_stats != null && stat.file_stats.Length > 0)
|
||||
break;
|
||||
|
||||
if (DateTime.Now > deadline)
|
||||
{
|
||||
_ = Http.Post($"{ts.host}/torrents", BuildRemovePayload(hash), headers: ts.headers);
|
||||
return OnError("jacktor", proxyManager);
|
||||
}
|
||||
|
||||
await Task.Delay(250);
|
||||
}
|
||||
|
||||
_ = Http.Post($"{ts.host}/torrents", BuildRemovePayload(hash), headers: ts.headers);
|
||||
|
||||
fileStats = stat.file_stats;
|
||||
hybridCache.Set(memKey, fileStats, DateTime.Now.AddHours(36));
|
||||
}
|
||||
|
||||
if (fileStats == null || fileStats.Length == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
var episodeTpl = new EpisodeTpl();
|
||||
int appended = 0;
|
||||
|
||||
foreach (var file in fileStats.OrderBy(i => i.Id))
|
||||
{
|
||||
if (!IsVideoFile(file.Path))
|
||||
continue;
|
||||
|
||||
episodeTpl.Append(
|
||||
Path.GetFileName(file.Path),
|
||||
title ?? original_title,
|
||||
s.ToString(),
|
||||
file.Id.ToString(),
|
||||
accsArgs($"{host}/jacktor/s{rid}?tsid={file.Id}"));
|
||||
|
||||
appended++;
|
||||
}
|
||||
|
||||
if (appended == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
return rjson
|
||||
? Content(episodeTpl.ToJson(), "application/json; charset=utf-8")
|
||||
: Content(episodeTpl.ToHtml(), "text/html; charset=utf-8");
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("jacktor/s{rid}")]
|
||||
async public ValueTask<ActionResult> Stream(string rid, int tsid = -1, string account_email = null)
|
||||
{
|
||||
var init = await loadKit(ModInit.Settings);
|
||||
if (!init.enable)
|
||||
return Forbid();
|
||||
|
||||
if (NoAccessGroup(init, out string error_msg))
|
||||
return Json(new { accsdb = true, error_msg });
|
||||
|
||||
var invoke = new JackTorInvoke(init, hybridCache, OnLog, proxyManager);
|
||||
if (!invoke.TryGetSource(rid, out JackTorSourceCache source))
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
int index = tsid != -1 ? tsid : 1;
|
||||
string country = requestInfo.Country;
|
||||
|
||||
async ValueTask<ActionResult> AuthStream(string tsHost, string login, string passwd, string uhost = null, Dictionary<string, string> addheaders = null)
|
||||
{
|
||||
string memKey = $"jacktor:auth_stream:{rid}:{uhost ?? tsHost}";
|
||||
if (!hybridCache.TryGetValue(memKey, out string hash))
|
||||
{
|
||||
login = (login ?? string.Empty).Replace("{account_email}", account_email ?? string.Empty);
|
||||
|
||||
var headers = HeadersModel.Init("Authorization", $"Basic {CrypTo.Base64($"{login}:{passwd}")}");
|
||||
headers = HeadersModel.Join(headers, addheaders);
|
||||
|
||||
string response = await Http.Post(
|
||||
$"{tsHost}/torrents",
|
||||
BuildAddPayload(source.SourceUri),
|
||||
timeoutSeconds: 5,
|
||||
headers: headers);
|
||||
|
||||
hash = ExtractHash(response);
|
||||
if (string.IsNullOrWhiteSpace(hash))
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
hybridCache.Set(memKey, hash, DateTime.Now.AddMinutes(1));
|
||||
}
|
||||
|
||||
return Redirect($"{uhost ?? tsHost}/stream?link={hash}&index={index}&play");
|
||||
}
|
||||
|
||||
if ((init.torrs == null || init.torrs.Length == 0) && (init.auth_torrs == null || init.auth_torrs.Count == 0))
|
||||
{
|
||||
if (TryReadLocalTorrServerPassword(out string localPassword))
|
||||
{
|
||||
return await AuthStream(
|
||||
$"http://{AppInit.conf.listen.localhost}:9080",
|
||||
"ts",
|
||||
localPassword,
|
||||
uhost: $"{host}/ts");
|
||||
}
|
||||
|
||||
return Redirect($"{host}/ts/stream?link={HttpUtility.UrlEncode(source.SourceUri)}&index={index}&play");
|
||||
}
|
||||
|
||||
if (init.auth_torrs != null && init.auth_torrs.Count > 0)
|
||||
{
|
||||
string tsKey = $"jacktor:ts2:{rid}:{requestInfo.IP}";
|
||||
if (!hybridCache.TryGetValue(tsKey, out PidTorAuthTS ts))
|
||||
{
|
||||
var servers = init.auth_torrs.Where(i => i.enable).ToList();
|
||||
if (country != null)
|
||||
{
|
||||
servers = servers
|
||||
.Where(i => i.country == null || i.country.Contains(country))
|
||||
.Where(i => i.no_country == null || !i.no_country.Contains(country))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (servers.Count == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
ts = servers[Random.Shared.Next(0, servers.Count)];
|
||||
hybridCache.Set(tsKey, ts, DateTime.Now.AddHours(4));
|
||||
}
|
||||
|
||||
return await AuthStream(ts.host, ts.login, ts.passwd, addheaders: ts.headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (init.base_auth != null && init.base_auth.enable)
|
||||
{
|
||||
if (init.torrs == null || init.torrs.Length == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
string tsKey = $"jacktor:ts3:{rid}:{requestInfo.IP}";
|
||||
if (!hybridCache.TryGetValue(tsKey, out string tsHost))
|
||||
{
|
||||
tsHost = init.torrs[Random.Shared.Next(0, init.torrs.Length)];
|
||||
hybridCache.Set(tsKey, tsHost, DateTime.Now.AddHours(4));
|
||||
}
|
||||
|
||||
return await AuthStream(tsHost, init.base_auth.login, init.base_auth.passwd, addheaders: init.base_auth.headers);
|
||||
}
|
||||
|
||||
if (init.torrs == null || init.torrs.Length == 0)
|
||||
return OnError("jacktor", proxyManager);
|
||||
|
||||
string key = $"jacktor:ts4:{rid}:{requestInfo.IP}";
|
||||
if (!hybridCache.TryGetValue(key, out string torrentHost))
|
||||
{
|
||||
torrentHost = init.torrs[Random.Shared.Next(0, init.torrs.Length)];
|
||||
hybridCache.Set(key, torrentHost, DateTime.Now.AddHours(4));
|
||||
}
|
||||
|
||||
return Redirect($"{torrentHost}/stream?link={HttpUtility.UrlEncode(source.SourceUri)}&index={index}&play");
|
||||
}
|
||||
}
|
||||
|
||||
private (List<HeadersModel> headers, string host) ResolveProbeTorrentServer(JackTorSettings init, string account_email)
|
||||
{
|
||||
if ((init.torrs == null || init.torrs.Length == 0) && (init.auth_torrs == null || init.auth_torrs.Count == 0))
|
||||
{
|
||||
if (TryReadLocalTorrServerPassword(out string localPassword))
|
||||
{
|
||||
var headers = HeadersModel.Init("Authorization", $"Basic {CrypTo.Base64($"ts:{localPassword}")}");
|
||||
return (headers, $"http://{AppInit.conf.listen.localhost}:9080");
|
||||
}
|
||||
|
||||
return (null, $"http://{AppInit.conf.listen.localhost}:9080");
|
||||
}
|
||||
|
||||
if (init.auth_torrs != null && init.auth_torrs.Count > 0)
|
||||
{
|
||||
var ts = init.auth_torrs.FirstOrDefault(i => i.enable) ?? init.auth_torrs.First();
|
||||
string login = (ts.login ?? string.Empty).Replace("{account_email}", account_email ?? string.Empty);
|
||||
var auth = HeadersModel.Init("Authorization", $"Basic {CrypTo.Base64($"{login}:{ts.passwd}")}");
|
||||
|
||||
return (httpHeaders(ts.host, HeadersModel.Join(auth, ts.headers)), ts.host);
|
||||
}
|
||||
|
||||
if (init.base_auth != null && init.base_auth.enable)
|
||||
{
|
||||
string tsHost = init.torrs?.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(tsHost))
|
||||
return (null, null);
|
||||
|
||||
string login = (init.base_auth.login ?? string.Empty).Replace("{account_email}", account_email ?? string.Empty);
|
||||
var auth = HeadersModel.Init("Authorization", $"Basic {CrypTo.Base64($"{login}:{init.base_auth.passwd}")}");
|
||||
|
||||
return (httpHeaders(tsHost, HeadersModel.Join(auth, init.base_auth.headers)), tsHost);
|
||||
}
|
||||
|
||||
return (null, init.torrs?.FirstOrDefault());
|
||||
}
|
||||
|
||||
private bool TryReadLocalTorrServerPassword(out string password)
|
||||
{
|
||||
password = null;
|
||||
|
||||
if (!System.IO.File.Exists("torrserver/accs.db"))
|
||||
return false;
|
||||
|
||||
string accs = System.IO.File.ReadAllText("torrserver/accs.db");
|
||||
password = Regex.Match(accs, "\"ts\":\"([^\"]+)\"").Groups[1].Value;
|
||||
return !string.IsNullOrWhiteSpace(password);
|
||||
}
|
||||
|
||||
private static string BuildAddPayload(string sourceUri)
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
action = "add",
|
||||
link = sourceUri,
|
||||
title = string.Empty,
|
||||
poster = string.Empty,
|
||||
save_to_db = false
|
||||
});
|
||||
}
|
||||
|
||||
private static string BuildGetPayload(string hash)
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
action = "get",
|
||||
hash
|
||||
});
|
||||
}
|
||||
|
||||
private static string BuildRemovePayload(string hash)
|
||||
{
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
action = "rem",
|
||||
hash
|
||||
});
|
||||
}
|
||||
|
||||
private static string ExtractHash(string response)
|
||||
{
|
||||
return Regex.Match(response ?? string.Empty, "\"hash\":\"([^\"]+)\"").Groups[1].Value;
|
||||
}
|
||||
|
||||
private static bool IsVideoFile(string path)
|
||||
{
|
||||
string ext = (Path.GetExtension(path) ?? string.Empty).ToLowerInvariant();
|
||||
return ext switch
|
||||
{
|
||||
".srt" => false,
|
||||
".txt" => false,
|
||||
".jpg" => false,
|
||||
".jpeg" => false,
|
||||
".png" => false,
|
||||
".nfo" => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
15
JackTor/JackTor.csproj
Normal file
15
JackTor/JackTor.csproj
Normal 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>
|
||||
612
JackTor/JackTorInvoke.cs
Normal file
612
JackTor/JackTorInvoke.cs
Normal file
@ -0,0 +1,612 @@
|
||||
using JackTor.Models;
|
||||
using Shared.Engine;
|
||||
using Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace JackTor
|
||||
{
|
||||
public class JackTorInvoke
|
||||
{
|
||||
private readonly JackTorSettings _init;
|
||||
private readonly IHybridCache _hybridCache;
|
||||
private readonly Action<string> _onLog;
|
||||
private readonly ProxyManager _proxyManager;
|
||||
|
||||
private static readonly (string token, string label)[] _voiceTokens = new[]
|
||||
{
|
||||
("ukr", "Українська"),
|
||||
("укр", "Українська"),
|
||||
("україн", "Українська"),
|
||||
("eng", "Англійська"),
|
||||
("rus", "Російська"),
|
||||
("дубляж", "Дубляж"),
|
||||
("dub", "Дубляж"),
|
||||
("mvo", "Багатоголосий"),
|
||||
("lostfilm", "LostFilm"),
|
||||
("newstudio", "NewStudio"),
|
||||
("hdrezka", "HDRezka"),
|
||||
("anilibria", "AniLibria")
|
||||
};
|
||||
|
||||
public JackTorInvoke(JackTorSettings init, IHybridCache hybridCache, Action<string> onLog, ProxyManager proxyManager)
|
||||
{
|
||||
_init = init;
|
||||
_hybridCache = hybridCache;
|
||||
_onLog = onLog;
|
||||
_proxyManager = proxyManager;
|
||||
}
|
||||
|
||||
public async Task<List<JackTorParsedResult>> Search(string title, string originalTitle, int year, int serial, string originalLanguage)
|
||||
{
|
||||
string memKey = $"jacktor:search:{serial}:{year}:{(title ?? string.Empty).Trim().ToLowerInvariant()}:{(originalTitle ?? string.Empty).Trim().ToLowerInvariant()}";
|
||||
if (_hybridCache.TryGetValue(memKey, out List<JackTorParsedResult> cached))
|
||||
return cached;
|
||||
|
||||
var queries = BuildQueries(title, originalTitle, year);
|
||||
if (queries.Count == 0)
|
||||
return new List<JackTorParsedResult>();
|
||||
|
||||
var rawResults = new List<JackettResult>(256);
|
||||
int categoryId = ResolveCategory(serial, originalLanguage);
|
||||
|
||||
foreach (string query in queries)
|
||||
{
|
||||
var chunk = await SearchRaw(query, categoryId);
|
||||
if (chunk != null && chunk.Count > 0)
|
||||
rawResults.AddRange(chunk);
|
||||
}
|
||||
|
||||
var normalized = NormalizeAndFilter(rawResults, year, serial);
|
||||
CacheSources(normalized);
|
||||
_hybridCache.Set(memKey, normalized, DateTime.Now.AddMinutes(5));
|
||||
|
||||
_onLog?.Invoke($"JackTor: отримано {rawResults.Count} сирих результатів, після фільтрації лишилось {normalized.Count}");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public bool TryGetSource(string rid, out JackTorSourceCache source)
|
||||
{
|
||||
return _hybridCache.TryGetValue($"jacktor:source:{rid}", out source);
|
||||
}
|
||||
|
||||
private async Task<List<JackettResult>> SearchRaw(string query, int categoryId)
|
||||
{
|
||||
string jackettHost = (_init.jackett ?? _init.host ?? string.Empty).Trim().TrimEnd('/');
|
||||
if (string.IsNullOrWhiteSpace(jackettHost))
|
||||
return null;
|
||||
|
||||
string apikeyArg = string.IsNullOrWhiteSpace(_init.apikey) ? string.Empty : $"apikey={HttpUtility.UrlEncode(_init.apikey)}&";
|
||||
string categoryArg = categoryId > 0 ? $"&Category[]={categoryId}" : string.Empty;
|
||||
|
||||
string url = $"{jackettHost}/api/v2.0/indexers/all/results?{apikeyArg}Query={HttpUtility.UrlEncode(query)}{categoryArg}";
|
||||
var headers = new List<HeadersModel>()
|
||||
{
|
||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
||||
new HeadersModel("Referer", $"{jackettHost}/")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_onLog?.Invoke($"JackTor: запит до Jackett -> {query}");
|
||||
var root = await Http.Get<JackettSearchRoot>(
|
||||
_init.cors(url),
|
||||
timeoutSeconds: _init.httptimeout > 0 ? _init.httptimeout : 12,
|
||||
headers: headers,
|
||||
proxy: _proxyManager.Get()
|
||||
);
|
||||
|
||||
if (root?.Results == null || root.Results.Length == 0)
|
||||
return null;
|
||||
|
||||
return root.Results.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog?.Invoke($"JackTor: помилка запиту до Jackett ({query}) -> {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> BuildQueries(string title, string originalTitle, int year)
|
||||
{
|
||||
string mode = (_init.query_mode ?? "both").Trim().ToLowerInvariant();
|
||||
string cleanTitle = (title ?? string.Empty).Trim();
|
||||
string cleanOriginal = (originalTitle ?? string.Empty).Trim();
|
||||
|
||||
var queries = new List<string>(6);
|
||||
var unique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void Add(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return;
|
||||
|
||||
string normalized = Regex.Replace(query, "\\s{2,}", " ").Trim();
|
||||
if (normalized.Length < 2)
|
||||
return;
|
||||
|
||||
if (unique.Add(normalized))
|
||||
queries.Add(normalized);
|
||||
}
|
||||
|
||||
void AddWithYear(string value)
|
||||
{
|
||||
Add(value);
|
||||
if (year > 1900)
|
||||
Add($"{value} {year}");
|
||||
}
|
||||
|
||||
if (mode == "original")
|
||||
{
|
||||
AddWithYear(cleanOriginal);
|
||||
AddWithYear(cleanTitle);
|
||||
}
|
||||
else if (mode == "title")
|
||||
{
|
||||
AddWithYear(cleanTitle);
|
||||
AddWithYear(cleanOriginal);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddWithYear(cleanOriginal);
|
||||
AddWithYear(cleanTitle);
|
||||
Add(cleanOriginal);
|
||||
Add(cleanTitle);
|
||||
}
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
private int ResolveCategory(int serial, string originalLanguage)
|
||||
{
|
||||
string lang = (originalLanguage ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (lang == "ja" || lang == "jp" || lang.StartsWith("ja-"))
|
||||
return 0;
|
||||
|
||||
return serial == 1 ? 5000 : 2000;
|
||||
}
|
||||
|
||||
private List<JackTorParsedResult> NormalizeAndFilter(List<JackettResult> rawResults, int year, int serial)
|
||||
{
|
||||
if (rawResults == null || rawResults.Count == 0)
|
||||
return new List<JackTorParsedResult>();
|
||||
|
||||
int yearTolerance = _init.year_tolerance < 0 ? 0 : _init.year_tolerance;
|
||||
var allowTrackers = (_init.trackers_allow ?? Array.Empty<string>())
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim().ToLowerInvariant())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var blockTrackers = (_init.trackers_block ?? Array.Empty<string>())
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim().ToLowerInvariant())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var byRid = new Dictionary<string, JackTorParsedResult>(StringComparer.OrdinalIgnoreCase);
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
foreach (var raw in rawResults)
|
||||
{
|
||||
if (raw == null || string.IsNullOrWhiteSpace(raw.Title))
|
||||
continue;
|
||||
|
||||
string source = !string.IsNullOrWhiteSpace(raw.MagnetUri) ? raw.MagnetUri : raw.Link;
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
continue;
|
||||
|
||||
string tracker = (raw.Tracker ?? string.Empty).Trim();
|
||||
string trackerId = (raw.TrackerId ?? string.Empty).Trim();
|
||||
string trackerKey = !string.IsNullOrWhiteSpace(trackerId) ? trackerId.ToLowerInvariant() : tracker.ToLowerInvariant();
|
||||
|
||||
if (allowTrackers.Count > 0 && !allowTrackers.Contains(trackerKey) && !allowTrackers.Contains(tracker.ToLowerInvariant()))
|
||||
continue;
|
||||
|
||||
if (blockTrackers.Contains(trackerKey) || blockTrackers.Contains(tracker.ToLowerInvariant()))
|
||||
continue;
|
||||
|
||||
if (raw.Seeders < _init.min_sid)
|
||||
continue;
|
||||
|
||||
if (raw.Peers < _init.min_peers)
|
||||
continue;
|
||||
|
||||
if (_init.max_serial_size > 0 && _init.max_size > 0)
|
||||
{
|
||||
if (serial == 1)
|
||||
{
|
||||
if (raw.Size > _init.max_serial_size)
|
||||
continue;
|
||||
}
|
||||
else if (raw.Size > _init.max_size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (_init.max_size > 0 && raw.Size > _init.max_size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_init.max_age_days > 0 && raw.PublishDate > DateTime.MinValue)
|
||||
{
|
||||
DateTime pubUtc = raw.PublishDate.Kind == DateTimeKind.Unspecified
|
||||
? DateTime.SpecifyKind(raw.PublishDate, DateTimeKind.Utc)
|
||||
: raw.PublishDate.ToUniversalTime();
|
||||
|
||||
if ((now - pubUtc).TotalDays > _init.max_age_days)
|
||||
continue;
|
||||
}
|
||||
|
||||
string searchable = $"{raw.Title} {raw.Description} {raw.CategoryDesc}";
|
||||
int quality = ParseQuality(searchable);
|
||||
if (!_init.forceAll && quality == 0)
|
||||
continue;
|
||||
|
||||
if (_init.quality_allow != null && _init.quality_allow.Length > 0 && quality > 0 && !_init.quality_allow.Contains(quality))
|
||||
continue;
|
||||
|
||||
string codec = ParseCodec(searchable);
|
||||
bool isHdr = IsHdr(searchable);
|
||||
bool isDolbyVision = Regex.IsMatch(searchable, "dolby\\s*vision", RegexOptions.IgnoreCase);
|
||||
|
||||
if (!IsHdrModeAllowed(isHdr))
|
||||
continue;
|
||||
|
||||
if (!IsCodecAllowed(codec))
|
||||
continue;
|
||||
|
||||
int extractedYear = ExtractYear(searchable);
|
||||
if (year > 1900 && extractedYear > 1900 && Math.Abs(extractedYear - year) > yearTolerance)
|
||||
continue;
|
||||
|
||||
int[] seasons = ParseSeasons(searchable);
|
||||
bool serialHint = IsSerialHint(searchable);
|
||||
bool movieHint = IsMovieHint(searchable);
|
||||
|
||||
if (serial == 1)
|
||||
{
|
||||
if (!serialHint && seasons.Length == 0 && movieHint && !_init.forceAll)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (serialHint && !movieHint && !_init.forceAll)
|
||||
continue;
|
||||
}
|
||||
|
||||
string voice = ParseVoice(searchable, trackerKey);
|
||||
if (_init.emptyVoice == false && string.IsNullOrWhiteSpace(voice))
|
||||
continue;
|
||||
|
||||
string regexSource = $"{raw.Title}:{voice}";
|
||||
if (!string.IsNullOrWhiteSpace(_init.filter) && !IsRegexMatch(regexSource, _init.filter))
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_init.filter_ignore) && IsRegexMatch(regexSource, _init.filter_ignore))
|
||||
continue;
|
||||
|
||||
string rid = BuildRid(raw.InfoHash, source);
|
||||
|
||||
var parsed = new JackTorParsedResult()
|
||||
{
|
||||
Rid = rid,
|
||||
Title = raw.Title.Trim(),
|
||||
Tracker = tracker,
|
||||
TrackerId = trackerId,
|
||||
SourceUri = source.Trim(),
|
||||
Voice = voice,
|
||||
AudioRank = CalculateAudioRank(voice),
|
||||
Quality = quality,
|
||||
QualityLabel = quality > 0 ? $"{quality}p" : "невідома",
|
||||
MediaInfo = BuildMediaInfo(raw.Size, codec, isHdr, isDolbyVision),
|
||||
CategoryDesc = raw.CategoryDesc,
|
||||
Codec = codec,
|
||||
IsHdr = isHdr,
|
||||
IsDolbyVision = isDolbyVision,
|
||||
Seeders = raw.Seeders,
|
||||
Peers = raw.Peers,
|
||||
Size = raw.Size,
|
||||
PublishDate = raw.PublishDate,
|
||||
Seasons = seasons,
|
||||
ExtractedYear = extractedYear,
|
||||
Gain = raw.Gain
|
||||
};
|
||||
|
||||
if (byRid.TryGetValue(rid, out JackTorParsedResult exists))
|
||||
{
|
||||
if (Score(parsed) > Score(exists))
|
||||
byRid[rid] = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
byRid[rid] = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
var result = byRid.Values;
|
||||
|
||||
IOrderedEnumerable<JackTorParsedResult> ordered = result
|
||||
.OrderByDescending(i => i.AudioRank)
|
||||
.ThenByDescending(i => !string.IsNullOrWhiteSpace(i.Voice))
|
||||
.ThenByDescending(i => i.SourceUri.StartsWith("magnet:?xt=urn:btih:", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
string sort = (_init.sort ?? "publishdate").Trim().ToLowerInvariant();
|
||||
if (sort == "size")
|
||||
{
|
||||
ordered = ordered.ThenByDescending(i => i.Size);
|
||||
}
|
||||
else if (sort == "sid")
|
||||
{
|
||||
ordered = ordered.ThenByDescending(i => i.Seeders).ThenByDescending(i => i.Peers);
|
||||
}
|
||||
else
|
||||
{
|
||||
ordered = ordered.ThenByDescending(i => i.PublishDate).ThenByDescending(i => i.Seeders);
|
||||
}
|
||||
|
||||
return ordered.ToList();
|
||||
}
|
||||
|
||||
private bool IsRegexMatch(string source, string pattern)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Regex.IsMatch(source ?? string.Empty, pattern, RegexOptions.IgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onLog?.Invoke($"JackTor: помилка regex '{pattern}' -> {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private int ParseQuality(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return 0;
|
||||
|
||||
if (Regex.IsMatch(text, "(2160p|\\b4k\\b|\\buhd\\b)", RegexOptions.IgnoreCase))
|
||||
return 2160;
|
||||
|
||||
if (Regex.IsMatch(text, "1080p", RegexOptions.IgnoreCase))
|
||||
return 1080;
|
||||
|
||||
if (Regex.IsMatch(text, "720p", RegexOptions.IgnoreCase))
|
||||
return 720;
|
||||
|
||||
if (Regex.IsMatch(text, "480p", RegexOptions.IgnoreCase))
|
||||
return 480;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private string ParseCodec(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return string.Empty;
|
||||
|
||||
if (Regex.IsMatch(text, "(hevc|h\\.265|x265)", RegexOptions.IgnoreCase))
|
||||
return "H.265";
|
||||
|
||||
if (Regex.IsMatch(text, "(h\\.264|x264|avc)", RegexOptions.IgnoreCase))
|
||||
return "H.264";
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private bool IsHdr(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
return Regex.IsMatch(text, "(hdr10|\\bhdr\\b)", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private bool IsHdrModeAllowed(bool isHdr)
|
||||
{
|
||||
string mode = (_init.hdr_mode ?? "any").Trim().ToLowerInvariant();
|
||||
return mode switch
|
||||
{
|
||||
"hdr_only" => isHdr,
|
||||
"sdr_only" => !isHdr,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsCodecAllowed(string codec)
|
||||
{
|
||||
string mode = (_init.codec_allow ?? "any").Trim().ToLowerInvariant();
|
||||
return mode switch
|
||||
{
|
||||
"h265" => codec == "H.265",
|
||||
"h264" => codec == "H.264",
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private int ExtractYear(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return 0;
|
||||
|
||||
var match = Regex.Match(text, "\\b(19\\d{2}|20\\d{2})\\b");
|
||||
if (!match.Success)
|
||||
return 0;
|
||||
|
||||
if (int.TryParse(match.Value, out int year))
|
||||
return year;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int[] ParseSeasons(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return Array.Empty<int>();
|
||||
|
||||
var seasons = new HashSet<int>();
|
||||
|
||||
foreach (Match match in Regex.Matches(text, "\\b[sс](\\d{1,2})(?:e\\d{1,2})?\\b", RegexOptions.IgnoreCase))
|
||||
{
|
||||
if (int.TryParse(match.Groups[1].Value, out int season) && season > 0 && season < 100)
|
||||
seasons.Add(season);
|
||||
}
|
||||
|
||||
foreach (Match match in Regex.Matches(text, "(?:season|сезон)\\s*(\\d{1,2})", RegexOptions.IgnoreCase))
|
||||
{
|
||||
if (int.TryParse(match.Groups[1].Value, out int season) && season > 0 && season < 100)
|
||||
seasons.Add(season);
|
||||
}
|
||||
|
||||
return seasons.OrderBy(i => i).ToArray();
|
||||
}
|
||||
|
||||
private bool IsSerialHint(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
return Regex.IsMatch(text, "(\\b[sс]\\d{1,2}e\\d{1,2}\\b|season|сезон|episodes|серії|серии|\\btv\\b)", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private bool IsMovieHint(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
return Regex.IsMatch(text, "(movies|movie|film|фільм|кино)", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private string ParseVoice(string text, string trackerKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
text = string.Empty;
|
||||
|
||||
string lowered = text.ToLowerInvariant();
|
||||
var voices = new List<string>();
|
||||
|
||||
foreach (var (token, label) in _voiceTokens)
|
||||
{
|
||||
if (lowered.Contains(token) && !voices.Contains(label))
|
||||
voices.Add(label);
|
||||
}
|
||||
|
||||
if (voices.Count == 0 && trackerKey == "toloka")
|
||||
voices.Add("Українська");
|
||||
|
||||
return string.Join(", ", voices);
|
||||
}
|
||||
|
||||
private int CalculateAudioRank(string voice)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(voice))
|
||||
return 0;
|
||||
|
||||
var prefs = _init.audio_pref ?? Array.Empty<string>();
|
||||
if (prefs.Length == 0)
|
||||
return 0;
|
||||
|
||||
string lowered = voice.ToLowerInvariant();
|
||||
int best = 0;
|
||||
|
||||
for (int i = 0; i < prefs.Length; i++)
|
||||
{
|
||||
string pref = (prefs[i] ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(pref))
|
||||
continue;
|
||||
|
||||
if (lowered.Contains(pref))
|
||||
{
|
||||
int rank = prefs.Length - i;
|
||||
if (rank > best)
|
||||
best = rank;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private string BuildMediaInfo(long size, string codec, bool isHdr, bool isDolbyVision)
|
||||
{
|
||||
var parts = new List<string>(4)
|
||||
{
|
||||
FormatSize(size)
|
||||
};
|
||||
|
||||
parts.Add(isHdr ? "HDR" : "SDR");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(codec))
|
||||
parts.Add(codec);
|
||||
|
||||
if (isDolbyVision)
|
||||
parts.Add("Dolby Vision");
|
||||
|
||||
return string.Join(" / ", parts.Where(i => !string.IsNullOrWhiteSpace(i)));
|
||||
}
|
||||
|
||||
private string FormatSize(long size)
|
||||
{
|
||||
if (size <= 0)
|
||||
return "розмір невідомий";
|
||||
|
||||
double gb = size / 1073741824d;
|
||||
if (gb >= 1)
|
||||
return $"{gb:0.##} GB";
|
||||
|
||||
double mb = size / 1048576d;
|
||||
return $"{mb:0.##} MB";
|
||||
}
|
||||
|
||||
private int Score(JackTorParsedResult item)
|
||||
{
|
||||
int qualityScore = item.Quality * 100;
|
||||
int seedScore = item.Seeders * 20 + item.Peers * 5;
|
||||
int audioScore = item.AudioRank * 200;
|
||||
int gainScore = (int)Math.Max(0, Math.Min(item.Gain, 1000));
|
||||
int dateScore = item.PublishDate > DateTime.MinValue ? 100 : 0;
|
||||
|
||||
return qualityScore + seedScore + audioScore + gainScore + dateScore;
|
||||
}
|
||||
|
||||
private string BuildRid(string infoHash, string source)
|
||||
{
|
||||
string hash = (infoHash ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(hash))
|
||||
return hash;
|
||||
|
||||
byte[] sourceBytes = Encoding.UTF8.GetBytes(source ?? string.Empty);
|
||||
byte[] digest = SHA1.HashData(sourceBytes);
|
||||
return Convert.ToHexString(digest).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private void CacheSources(List<JackTorParsedResult> items)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return;
|
||||
|
||||
DateTime expires = DateTime.Now.AddHours(36);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var cacheItem = new JackTorSourceCache()
|
||||
{
|
||||
Rid = item.Rid,
|
||||
SourceUri = item.SourceUri,
|
||||
Title = item.Title,
|
||||
Voice = item.Voice,
|
||||
Quality = item.Quality,
|
||||
Seasons = item.Seasons
|
||||
};
|
||||
|
||||
_hybridCache.Set($"jacktor:source:{item.Rid}", cacheItem, expires);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
JackTor/ModInit.cs
Normal file
191
JackTor/ModInit.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using JackTor.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Shared;
|
||||
using Shared.Engine;
|
||||
using Shared.Models.Module;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace JackTor
|
||||
{
|
||||
public class ModInit
|
||||
{
|
||||
public static double Version => 1.0;
|
||||
|
||||
public static JackTorSettings JackTor;
|
||||
|
||||
public static JackTorSettings Settings
|
||||
{
|
||||
get => JackTor;
|
||||
set => JackTor = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Модуль завантажено.
|
||||
/// </summary>
|
||||
public static void loaded(InitspaceModel initspace)
|
||||
{
|
||||
JackTor = new JackTorSettings("JackTor", "http://127.0.0.1:9117", streamproxy: false, useproxy: false)
|
||||
{
|
||||
displayname = "JackTor",
|
||||
displayindex = 0,
|
||||
group = 0,
|
||||
group_hide = true,
|
||||
|
||||
jackett = "http://127.0.0.1:9117",
|
||||
apikey = string.Empty,
|
||||
min_sid = 5,
|
||||
min_peers = 0,
|
||||
max_size = 0,
|
||||
max_serial_size = 0,
|
||||
emptyVoice = true,
|
||||
forceAll = false,
|
||||
sort = "sid",
|
||||
max_age_days = 0,
|
||||
quality_allow = new[] { 2160, 1080, 720 },
|
||||
trackers_allow = Array.Empty<string>(),
|
||||
trackers_block = Array.Empty<string>(),
|
||||
hdr_mode = "any",
|
||||
codec_allow = "any",
|
||||
audio_pref = new[] { "ukr", "eng", "rus" },
|
||||
year_tolerance = 1,
|
||||
query_mode = "both",
|
||||
proxy = new Shared.Models.Base.ProxySettings()
|
||||
{
|
||||
useAuth = true,
|
||||
username = "",
|
||||
password = "",
|
||||
list = new string[] { "socks5://ip:port" }
|
||||
}
|
||||
};
|
||||
|
||||
var conf = ModuleInvoke.Conf("JackTor", JackTor) ?? JObject.FromObject(JackTor);
|
||||
JackTor = conf.ToObject<JackTorSettings>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(JackTor.jackett))
|
||||
JackTor.jackett = JackTor.host;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(JackTor.host))
|
||||
JackTor.host = JackTor.jackett;
|
||||
|
||||
// Показувати «уточнити пошук».
|
||||
AppInit.conf.online.with_search.Add("jacktor");
|
||||
}
|
||||
}
|
||||
|
||||
public static class UpdateService
|
||||
{
|
||||
private static readonly string _connectUrl = "https://lmcuk.lampame.v6.rocks/stats";
|
||||
|
||||
private static ConnectResponse Connect = null;
|
||||
private static DateTime? _connectTime = null;
|
||||
private static DateTime? _disconnectTime = null;
|
||||
|
||||
private static readonly TimeSpan _resetInterval = TimeSpan.FromHours(4);
|
||||
private static Timer _resetTimer = null;
|
||||
|
||||
private static readonly object _lock = new();
|
||||
|
||||
public static async Task ConnectAsync(string host, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_connectTime is not null || Connect?.IsUpdateUnavailable == true)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_connectTime is not null || Connect?.IsUpdateUnavailable == true)
|
||||
return;
|
||||
|
||||
_connectTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var handler = new SocketsHttpHandler
|
||||
{
|
||||
SslOptions = new SslClientAuthenticationOptions
|
||||
{
|
||||
RemoteCertificateValidationCallback = (_, _, _, _) => true,
|
||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
|
||||
}
|
||||
};
|
||||
|
||||
using var client = new HttpClient(handler);
|
||||
client.Timeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var request = new
|
||||
{
|
||||
Host = host,
|
||||
Module = ModInit.Settings.plugin,
|
||||
Version = ModInit.Version,
|
||||
};
|
||||
|
||||
var requestJson = JsonConvert.SerializeObject(request, Formatting.None);
|
||||
var requestContent = new StringContent(requestJson, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
var response = await client
|
||||
.PostAsync(_connectUrl, requestContent, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (response.Content.Headers.ContentLength > 0)
|
||||
{
|
||||
var responseText = await response.Content
|
||||
.ReadAsStringAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Connect = JsonConvert.DeserializeObject<ConnectResponse>(responseText);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_resetTimer?.Dispose();
|
||||
_resetTimer = null;
|
||||
|
||||
if (Connect?.IsUpdateUnavailable != true)
|
||||
{
|
||||
_resetTimer = new Timer(ResetConnectTime, null, _resetInterval, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
_disconnectTime = Connect?.IsNoiseEnabled == true
|
||||
? DateTime.UtcNow.AddHours(Random.Shared.Next(1, 4))
|
||||
: DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ResetConnectTime(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetConnectTime(object state)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_connectTime = null;
|
||||
Connect = null;
|
||||
|
||||
_resetTimer?.Dispose();
|
||||
_resetTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsDisconnected()
|
||||
{
|
||||
return _disconnectTime is not null
|
||||
&& DateTime.UtcNow >= _disconnectTime;
|
||||
}
|
||||
}
|
||||
|
||||
public record ConnectResponse(bool IsUpdateUnavailable, bool IsNoiseEnabled);
|
||||
}
|
||||
198
JackTor/Models/JackTorModels.cs
Normal file
198
JackTor/Models/JackTorModels.cs
Normal file
@ -0,0 +1,198 @@
|
||||
using Shared.Models.Online.Settings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace JackTor.Models
|
||||
{
|
||||
public class JackTorSettings : OnlinesSettings, ICloneable
|
||||
{
|
||||
public JackTorSettings(
|
||||
string plugin,
|
||||
string host,
|
||||
string apihost = null,
|
||||
bool useproxy = false,
|
||||
string token = null,
|
||||
bool enable = true,
|
||||
bool streamproxy = false,
|
||||
bool rip = false,
|
||||
bool forceEncryptToken = false,
|
||||
string rch_access = null,
|
||||
string stream_access = null)
|
||||
: base(plugin, host, apihost, useproxy, token, enable, streamproxy, rip, forceEncryptToken, rch_access, stream_access)
|
||||
{
|
||||
}
|
||||
|
||||
public string jackett { get; set; }
|
||||
|
||||
public string apikey { get; set; }
|
||||
|
||||
public int min_sid { get; set; }
|
||||
|
||||
public int min_peers { get; set; }
|
||||
|
||||
public long max_size { get; set; }
|
||||
|
||||
public long max_serial_size { get; set; }
|
||||
|
||||
public bool emptyVoice { get; set; }
|
||||
|
||||
public bool forceAll { get; set; }
|
||||
|
||||
public string filter { get; set; }
|
||||
|
||||
public string filter_ignore { get; set; }
|
||||
|
||||
public string sort { get; set; }
|
||||
|
||||
public int max_age_days { get; set; }
|
||||
|
||||
public string[] trackers_allow { get; set; }
|
||||
|
||||
public string[] trackers_block { get; set; }
|
||||
|
||||
public int[] quality_allow { get; set; }
|
||||
|
||||
public string hdr_mode { get; set; }
|
||||
|
||||
public string codec_allow { get; set; }
|
||||
|
||||
public string[] audio_pref { get; set; }
|
||||
|
||||
public int year_tolerance { get; set; }
|
||||
|
||||
public string query_mode { get; set; }
|
||||
|
||||
public PidTorAuthTS base_auth { get; set; }
|
||||
|
||||
public string[] torrs { get; set; }
|
||||
|
||||
public List<PidTorAuthTS> auth_torrs { get; set; }
|
||||
|
||||
public new JackTorSettings Clone()
|
||||
{
|
||||
return (JackTorSettings)MemberwiseClone();
|
||||
}
|
||||
|
||||
object ICloneable.Clone()
|
||||
{
|
||||
return MemberwiseClone();
|
||||
}
|
||||
}
|
||||
|
||||
public class JackettSearchRoot
|
||||
{
|
||||
public JackettResult[] Results { get; set; }
|
||||
|
||||
public JackettIndexer[] Indexers { get; set; }
|
||||
}
|
||||
|
||||
public class JackettResult
|
||||
{
|
||||
public string Tracker { get; set; }
|
||||
|
||||
public string TrackerId { get; set; }
|
||||
|
||||
public string TrackerType { get; set; }
|
||||
|
||||
public string CategoryDesc { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Link { get; set; }
|
||||
|
||||
public string Details { get; set; }
|
||||
|
||||
public DateTime PublishDate { get; set; }
|
||||
|
||||
public int[] Category { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public int Seeders { get; set; }
|
||||
|
||||
public int Peers { get; set; }
|
||||
|
||||
public string InfoHash { get; set; }
|
||||
|
||||
public string MagnetUri { get; set; }
|
||||
|
||||
public double Gain { get; set; }
|
||||
}
|
||||
|
||||
public class JackettIndexer
|
||||
{
|
||||
public string ID { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public int Results { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
|
||||
public int ElapsedTime { get; set; }
|
||||
}
|
||||
|
||||
public class JackTorParsedResult
|
||||
{
|
||||
public string Rid { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Tracker { get; set; }
|
||||
|
||||
public string TrackerId { get; set; }
|
||||
|
||||
public string SourceUri { get; set; }
|
||||
|
||||
public string Voice { get; set; }
|
||||
|
||||
public int AudioRank { get; set; }
|
||||
|
||||
public int Quality { get; set; }
|
||||
|
||||
public string QualityLabel { get; set; }
|
||||
|
||||
public string MediaInfo { get; set; }
|
||||
|
||||
public string CategoryDesc { get; set; }
|
||||
|
||||
public string Codec { get; set; }
|
||||
|
||||
public bool IsHdr { get; set; }
|
||||
|
||||
public bool IsDolbyVision { get; set; }
|
||||
|
||||
public int Seeders { get; set; }
|
||||
|
||||
public int Peers { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public DateTime PublishDate { get; set; }
|
||||
|
||||
public int[] Seasons { get; set; }
|
||||
|
||||
public int ExtractedYear { get; set; }
|
||||
|
||||
public double Gain { get; set; }
|
||||
}
|
||||
|
||||
public class JackTorSourceCache
|
||||
{
|
||||
public string Rid { get; set; }
|
||||
|
||||
public string SourceUri { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Voice { get; set; }
|
||||
|
||||
public int Quality { get; set; }
|
||||
|
||||
public int[] Seasons { get; set; }
|
||||
}
|
||||
}
|
||||
50
JackTor/OnlineApi.cs
Normal file
50
JackTor/OnlineApi.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Module;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace JackTor
|
||||
{
|
||||
public class OnlineApi
|
||||
{
|
||||
public static List<(string name, string url, string plugin, int index)> Invoke(
|
||||
HttpContext httpContext,
|
||||
IMemoryCache memoryCache,
|
||||
RequestModel requestInfo,
|
||||
string host,
|
||||
OnlineEventsModel args)
|
||||
{
|
||||
long.TryParse(args.id, out long tmdbid);
|
||||
return Events(host, tmdbid, args.imdb_id, args.kinopoisk_id, args.title, args.original_title, args.original_language, args.year, args.source, args.serial, args.account_email);
|
||||
}
|
||||
|
||||
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.JackTor;
|
||||
if (init.enable && !init.rip)
|
||||
{
|
||||
string url = init.overridehost;
|
||||
if (string.IsNullOrEmpty(url) || UpdateService.IsDisconnected())
|
||||
url = $"{host}/jacktor";
|
||||
|
||||
online.Add((init.displayname, url, "jacktor", init.displayindex));
|
||||
}
|
||||
|
||||
return online;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
JackTor/manifest.json
Normal file
6
JackTor/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"enable": true,
|
||||
"version": 3,
|
||||
"initspace": "JackTor.ModInit",
|
||||
"online": "JackTor.OnlineApi"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user