mirror of
https://github.com/lampame/lampac-ukraine.git
synced 2026-04-17 01:42:19 +00:00
Compare commits
No commits in common. "a7dbdbce2ee05c469d03dcf97a21967b3cde17f4" and "ae39beb8b606113ba89a2c8bfb50eceee90cd6e5" have entirely different histories.
a7dbdbce2e
...
ae39beb8b6
@ -1,478 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
string debugInfo = $"title={title}\noriginal_title={original_title}\nyear={year}\nserial={serial}\njackett={MaskSensitiveUrl(init.jackett)}\nmin_sid={init.min_sid}\nmin_peers={init.min_peers}";
|
|
||||||
return OnError("jacktor", refresh_proxy: true, weblog: debugInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 seasonLabel = (torrent.Seasons != null && torrent.Seasons.Length > 0)
|
|
||||||
? $"S{string.Join(",", torrent.Seasons.OrderBy(i => i))}"
|
|
||||||
: $"S{targetSeason}";
|
|
||||||
|
|
||||||
string releaseName = string.IsNullOrWhiteSpace(torrent.Voice)
|
|
||||||
? $"{seasonLabel} • {(torrent.Tracker ?? "Без назви")}"
|
|
||||||
: $"{seasonLabel} • {torrent.Voice}";
|
|
||||||
|
|
||||||
string qualityInfo = $"{torrent.Tracker} / {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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string MaskSensitiveUrl(string url)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return Regex.Replace(url, "(apikey=)[^&]+", "$1***", RegexOptions.IgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,739 +0,0 @@
|
|||||||
using JackTor.Models;
|
|
||||||
using Shared.Engine;
|
|
||||||
using Shared.Models;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawResults.Count == 0 && categoryId > 0)
|
|
||||||
{
|
|
||||||
foreach (string query in queries)
|
|
||||||
{
|
|
||||||
var chunk = await SearchRaw(query, 0);
|
|
||||||
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 rawJackett = (_init.jackett ?? _init.host ?? string.Empty).Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(rawJackett))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
string endpoint = BuildJackettEndpoint(rawJackett);
|
|
||||||
if (string.IsNullOrWhiteSpace(endpoint))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
string url = BuildJackettUrl(endpoint, query, categoryId);
|
|
||||||
string referer = BuildReferer(rawJackett);
|
|
||||||
var headers = new List<HeadersModel>()
|
|
||||||
{
|
|
||||||
new HeadersModel("User-Agent", "Mozilla/5.0"),
|
|
||||||
new HeadersModel("Referer", referer)
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_onLog?.Invoke($"JackTor: запит до Jackett -> {query} (cat={categoryId})");
|
|
||||||
int timeoutSeconds = Convert.ToInt32(_init.httptimeout);
|
|
||||||
if (timeoutSeconds <= 0)
|
|
||||||
timeoutSeconds = 12;
|
|
||||||
|
|
||||||
var (root, response) = await Http.BaseGetAsync<JackettSearchRoot>(
|
|
||||||
_init.cors(url),
|
|
||||||
timeoutSeconds: timeoutSeconds,
|
|
||||||
headers: headers,
|
|
||||||
proxy: _proxyManager.Get(),
|
|
||||||
statusCodeOK: false,
|
|
||||||
IgnoreDeserializeObject: true
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response == null || response.StatusCode != HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
_onLog?.Invoke($"JackTor: Jackett відповів статусом {(int?)response?.StatusCode} для {query}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 string BuildJackettEndpoint(string jackett)
|
|
||||||
{
|
|
||||||
string source = jackett.Trim().TrimEnd('/');
|
|
||||||
if (source.Contains("/api/v2.0/indexers/all/results", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return source;
|
|
||||||
|
|
||||||
return $"{source}/api/v2.0/indexers/all/results";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildJackettUrl(string endpoint, string query, int categoryId)
|
|
||||||
{
|
|
||||||
var args = new List<string>(8);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_init.apikey) && !HasParam(endpoint, "apikey"))
|
|
||||||
args.Add($"apikey={HttpUtility.UrlEncode(_init.apikey)}");
|
|
||||||
|
|
||||||
string encQuery = HttpUtility.UrlEncode(query ?? string.Empty);
|
|
||||||
args.Add($"Query={encQuery}");
|
|
||||||
args.Add($"query={encQuery}");
|
|
||||||
|
|
||||||
if (categoryId > 0)
|
|
||||||
{
|
|
||||||
args.Add($"Category[]={categoryId}");
|
|
||||||
args.Add($"cat={categoryId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string separator = endpoint.Contains("?") ? "&" : "?";
|
|
||||||
return endpoint + separator + string.Join("&", args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HasParam(string url, string name)
|
|
||||||
{
|
|
||||||
return Regex.IsMatch(url ?? string.Empty, $"[\\?&]{Regex.Escape(name)}=", RegexOptions.IgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildReferer(string jackett)
|
|
||||||
{
|
|
||||||
if (Uri.TryCreate(jackett, UriKind.Absolute, out Uri uri))
|
|
||||||
return $"{uri.Scheme}://{uri.Authority}/";
|
|
||||||
|
|
||||||
return jackett.Trim().TrimEnd('/') + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
int seeders = raw.Seeders ?? 0;
|
|
||||||
int peers = raw.Peers ?? 0;
|
|
||||||
long size = raw.Size ?? 0;
|
|
||||||
DateTime publishDate = raw.PublishDate ?? DateTime.MinValue;
|
|
||||||
double gain = raw.Gain ?? 0;
|
|
||||||
|
|
||||||
if (allowTrackers.Count > 0 && !allowTrackers.Contains(trackerKey) && !allowTrackers.Contains(tracker.ToLowerInvariant()))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (blockTrackers.Contains(trackerKey) || blockTrackers.Contains(tracker.ToLowerInvariant()))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (seeders < _init.min_sid)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (peers < _init.min_peers)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_init.max_serial_size > 0 && _init.max_size > 0)
|
|
||||||
{
|
|
||||||
if (serial == 1)
|
|
||||||
{
|
|
||||||
if (size > _init.max_serial_size)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (size > _init.max_size)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_init.max_size > 0 && size > _init.max_size)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_init.max_age_days > 0 && publishDate > DateTime.MinValue)
|
|
||||||
{
|
|
||||||
DateTime pubUtc = publishDate.Kind == DateTimeKind.Unspecified
|
|
||||||
? DateTime.SpecifyKind(publishDate, DateTimeKind.Utc)
|
|
||||||
: 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, raw.Guid, raw.Details, 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(size, codec, isHdr, isDolbyVision),
|
|
||||||
CategoryDesc = raw.CategoryDesc,
|
|
||||||
Codec = codec,
|
|
||||||
IsHdr = isHdr,
|
|
||||||
IsDolbyVision = isDolbyVision,
|
|
||||||
Seeders = seeders,
|
|
||||||
Peers = peers,
|
|
||||||
Size = size,
|
|
||||||
PublishDate = publishDate,
|
|
||||||
Seasons = seasons,
|
|
||||||
ExtractedYear = extractedYear,
|
|
||||||
Gain = gain
|
|
||||||
};
|
|
||||||
|
|
||||||
if (byRid.TryGetValue(rid, out JackTorParsedResult exists))
|
|
||||||
{
|
|
||||||
if (Score(parsed) > Score(exists))
|
|
||||||
byRid[rid] = parsed;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
byRid[rid] = parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = CollapseNearDuplicates(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 IEnumerable<JackTorParsedResult> CollapseNearDuplicates(IEnumerable<JackTorParsedResult> items)
|
|
||||||
{
|
|
||||||
var dedup = new Dictionary<string, JackTorParsedResult>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
string seasonKey = (item.Seasons == null || item.Seasons.Length == 0)
|
|
||||||
? "-"
|
|
||||||
: string.Join(",", item.Seasons.OrderBy(i => i));
|
|
||||||
|
|
||||||
string key = $"{item.Quality}|{item.Size}|{item.Seeders}|{item.Peers}|{seasonKey}|{NormalizeVoice(item.Voice)}|{NormalizeTitle(item.Title)}";
|
|
||||||
if (dedup.TryGetValue(key, out JackTorParsedResult exists))
|
|
||||||
{
|
|
||||||
if (Score(item) > Score(exists))
|
|
||||||
dedup[key] = item;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dedup[key] = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dedup.Values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string NormalizeVoice(string voice)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(voice))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return string.Join(",",
|
|
||||||
voice.ToLowerInvariant()
|
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(i => i.Trim())
|
|
||||||
.Distinct()
|
|
||||||
.OrderBy(i => i));
|
|
||||||
}
|
|
||||||
|
|
||||||
private string NormalizeTitle(string title)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(title))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
string lower = title.ToLowerInvariant();
|
|
||||||
lower = Regex.Replace(lower, "\\s+", " ").Trim();
|
|
||||||
return lower;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 guid, string details, string source)
|
|
||||||
{
|
|
||||||
string hash = (infoHash ?? string.Empty).Trim().ToLowerInvariant();
|
|
||||||
if (!string.IsNullOrWhiteSpace(hash))
|
|
||||||
return hash;
|
|
||||||
|
|
||||||
string stableId = !string.IsNullOrWhiteSpace(guid)
|
|
||||||
? guid.Trim()
|
|
||||||
: !string.IsNullOrWhiteSpace(details)
|
|
||||||
? details.Trim()
|
|
||||||
: source ?? string.Empty;
|
|
||||||
|
|
||||||
byte[] sourceBytes = Encoding.UTF8.GetBytes(stableId);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
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 Guid { get; set; }
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"version": 3,
|
|
||||||
"initspace": "JackTor.ModInit",
|
|
||||||
"online": "JackTor.OnlineApi"
|
|
||||||
}
|
|
||||||
69
README.md
69
README.md
@ -84,75 +84,6 @@ Parameter compatibility:
|
|||||||
- `webcorshost` does not conflict with `streamproxy`: CORS is used for parsing, `streamproxy` is used for streaming.
|
- `webcorshost` does not conflict with `streamproxy`: CORS is used for parsing, `streamproxy` is used for streaming.
|
||||||
- `webcorshost` does not conflict with `apn`: APN is used at the streaming stage, not for regular parsing.
|
- `webcorshost` does not conflict with `apn`: APN is used at the streaming stage, not for regular parsing.
|
||||||
|
|
||||||
## JackTor config example (`init.conf`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"JackTor": {
|
|
||||||
"enable": true,
|
|
||||||
"displayname": "JackTor",
|
|
||||||
"displayindex": 0,
|
|
||||||
|
|
||||||
"jackett": "jackett.app",
|
|
||||||
"apikey": "YOUR_JACKETT_API_KEY",
|
|
||||||
|
|
||||||
"min_sid": 5,
|
|
||||||
"min_peers": 0,
|
|
||||||
"max_size": 0,
|
|
||||||
"max_serial_size": 0,
|
|
||||||
"max_age_days": 0,
|
|
||||||
|
|
||||||
"forceAll": false,
|
|
||||||
"emptyVoice": true,
|
|
||||||
"sort": "sid",
|
|
||||||
"query_mode": "both",
|
|
||||||
"year_tolerance": 1,
|
|
||||||
|
|
||||||
"quality_allow": [2160, 1080, 720],
|
|
||||||
"hdr_mode": "any",
|
|
||||||
"codec_allow": "any",
|
|
||||||
"audio_pref": ["ukr", "eng", "rus"],
|
|
||||||
|
|
||||||
"trackers_allow": ["toloka", "rutracker", "noname-club"],
|
|
||||||
"trackers_block": ["selezen"],
|
|
||||||
|
|
||||||
"filter": "",
|
|
||||||
"filter_ignore": "(camrip|ts|telesync)",
|
|
||||||
|
|
||||||
"torrs": [
|
|
||||||
"http://127.0.0.1:8090"
|
|
||||||
],
|
|
||||||
"auth_torrs": [
|
|
||||||
{
|
|
||||||
"enable": true,
|
|
||||||
"host": "http://ts.example.com:8090",
|
|
||||||
"login": "{account_email}",
|
|
||||||
"passwd": "StrongPassword",
|
|
||||||
"country": "UA",
|
|
||||||
"no_country": null,
|
|
||||||
"headers": {
|
|
||||||
"x-api-key": "your-ts-key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"base_auth": {
|
|
||||||
"enable": false,
|
|
||||||
"login": "{account_email}",
|
|
||||||
"passwd": "StrongPassword",
|
|
||||||
"headers": {}
|
|
||||||
},
|
|
||||||
|
|
||||||
"group": 0,
|
|
||||||
"group_hide": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Key parameters at a glance:
|
|
||||||
- `jackett` + `apikey`: your Jackett host and API key.
|
|
||||||
- `min_sid` / `min_peers` / `max_size` / `max_serial_size`: base torrent filters.
|
|
||||||
- `quality_allow`, `hdr_mode`, `codec_allow`, `audio_pref`: quality/codec/language prioritization.
|
|
||||||
- `torrs`, `auth_torrs`, `base_auth`: TorrServer nodes used for playback.
|
|
||||||
- `filter` / `filter_ignore`: regex filters for release title and voice labels.
|
|
||||||
|
|
||||||
## APN support
|
## APN support
|
||||||
|
|
||||||
Sources with APN support:
|
Sources with APN support:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user