commit f843f04fd412df7b41f29fb5a936aef623d9f2f2 Author: lampac-talks Date: Fri Jan 30 16:22:36 2026 +0300 chore: initial commit 154.3 Signed-off-by: lampac-talks diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e494c80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,343 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +Properties/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +!Build diff --git a/BaseModule/BaseModule.csproj b/BaseModule/BaseModule.csproj new file mode 100644 index 0000000..a644285 --- /dev/null +++ b/BaseModule/BaseModule.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + library + true + + + + + + + diff --git a/BaseModule/Controllers/AdminController.cs b/BaseModule/Controllers/AdminController.cs new file mode 100644 index 0000000..b3d1677 --- /dev/null +++ b/BaseModule/Controllers/AdminController.cs @@ -0,0 +1,724 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Models.Module; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IO = System.IO; + +namespace Lampac.Controllers +{ + public class AdminController : BaseController + { + #region TryAuthorizeAdmin + bool TryAuthorizeAdmin(string passwd, out ActionResult result) + { + result = null; + + if (AppInit.rootPasswd == "termux") + { + HttpContext.Response.Cookies.Append("passwd", "termux"); + return true; + } + + if (string.IsNullOrWhiteSpace(passwd)) + HttpContext.Request.Cookies.TryGetValue("passwd", out passwd); + + if (string.IsNullOrWhiteSpace(passwd)) + { + result = Redirect("/admin/auth"); + return false; + } + + string ipKey = $"Accsdb:auth:IP:{requestInfo.IP}"; + if (!memoryCache.TryGetValue(ipKey, out ConcurrentDictionary passwds)) + { + passwds = new ConcurrentDictionary(); + memoryCache.Set(ipKey, passwds, DateTime.Today.AddDays(1)); + } + + passwds.TryAdd(passwd, 0); + + if (passwds.Count > 10) + { + result = Content("Too many attempts, try again tomorrow."); + return false; + } + + if (AppInit.rootPasswd == passwd) + return true; + + HttpContext.Response.Cookies.Delete("passwd"); + result = Redirect("/admin/auth"); + return false; + } + #endregion + + #region admin / auth + [HttpGet] + [HttpPost] + [Route("/admin")] + [Route("/admin/auth")] + public ActionResult Authorization([FromForm]string parol) + { + string passwd = parol?.Trim(); + + if (string.IsNullOrWhiteSpace(passwd)) + HttpContext.Request.Cookies.TryGetValue("passwd", out passwd); + + if (string.IsNullOrWhiteSpace(passwd)) + { + string html = @" + + + + Authorization + + + + + +
+
+ +
+ + + +
+ +
Выполните одну из команд через ssh

+ cat /home/lampac/passwd

+ docker exec -it lampac cat passwd +
+ + + +"; + + return Content(html, "text/html; charset=utf-8"); + } + else + { + if (!TryAuthorizeAdmin(passwd, out ActionResult badresult)) + return badresult; + + HttpContext.Response.Cookies.Append("passwd", passwd); + return renderAdmin(); + } + } + + ActionResult renderAdmin() + { + string adminHtml = IO.File.Exists("wwwroot/mycontrol/index.html") ? IO.File.ReadAllText("wwwroot/mycontrol/index.html") : IO.File.ReadAllText("wwwroot/control/index.html"); + return Content(adminHtml, contentType: "text/html; charset=utf-8"); + } + #endregion + + + #region init + [HttpPost] + [Route("/admin/init/save")] + public ActionResult InitSave([FromForm]string json) + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + try + { + JsonConvert.DeserializeObject(json); + } + catch (Exception ex) { return Json(new { error = true, ex = ex.Message }); } + + var jo = JsonConvert.DeserializeObject(json); + + JToken users = null; + var accsdbNode = jo["accsdb"] as JObject; + if (accsdbNode != null) + { + var usersNode = accsdbNode["users"]; + if (usersNode != null) + { + users = usersNode.DeepClone(); + accsdbNode.Remove("users"); + + IO.File.WriteAllText("users.json", JsonConvert.SerializeObject(users, Formatting.Indented)); + } + } + + IO.File.WriteAllText("init.conf", JsonConvert.SerializeObject(jo, Formatting.Indented)); + + return Json(new { success = true }); + } + + [HttpGet] + [Route("/admin/init/custom")] + public ActionResult InitCustom() + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + string json = IO.File.Exists("init.conf") ? IO.File.ReadAllText("init.conf") : null; + if (json != null && !json.Trim().StartsWith("{")) + json = "{" + json + "}"; + + var ob = json != null ? JsonConvert.DeserializeObject(json) : new JObject { }; + return ContentTo(JsonConvert.SerializeObject(ob)); + } + + [HttpGet] + [Route("/admin/init/current")] + public ActionResult InitCurrent() + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + return Content(JsonConvert.SerializeObject(AppInit.conf), contentType: "application/json; charset=utf-8"); + } + + [HttpGet] + [Route("/admin/init/default")] + public ActionResult InitDefault() + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + return Content(JsonConvert.SerializeObject(new AppInit()), contentType: "application/json; charset=utf-8"); + } + + [HttpGet] + [Route("/admin/init/example")] + public ActionResult InitExample() + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + return Content(IO.File.Exists("example.conf") ? IO.File.ReadAllText("example.conf") : string.Empty); + } + #endregion + + #region sync/init + [HttpGet] + [Route("/admin/sync/init")] + public ActionResult Synchtml() + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + string html = @" + + + + Редактор sync.conf + + + + + +
+
+
+ + + +
+ + + + + +"; + + string conf = IO.File.Exists("sync.conf") ? IO.File.ReadAllText("sync.conf") : string.Empty; + return Content(html.Replace("{conf}", conf), contentType: "text/html; charset=utf-8"); + } + + + [HttpPost] + [Route("/admin/sync/init/save")] + public ActionResult SyncSave([FromForm] string json) + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + return badresult; + + try + { + string testjson = json.Trim(); + if (!testjson.StartsWith("{")) + testjson = "{" + testjson + "}"; + + JsonConvert.DeserializeObject(testjson); + + } + catch (Exception ex) { return Json(new { error = true, ex = ex.Message }); } + + IO.File.WriteAllText("sync.conf", json); + return Json(new { success = true }); + } + #endregion + + #region manifest + [HttpGet] + [HttpPost] + [Route("/admin/manifest/install")] + public Task ManifestInstallHtml(string online, string sisi, string jac, string dlna, string tracks, string ts, string catalog, string merch, string eng) + { + if (IO.File.Exists("module/manifest.json")) + { + if (!TryAuthorizeAdmin(null, out ActionResult badresult)) + { + HttpContext.Response.Redirect("/admin/auth"); + return Task.CompletedTask; + } + } + + HttpContext.Response.ContentType = "text/html; charset=utf-8"; + + if (AppInit.rootPasswd == "termux") + return HttpContext.Response.WriteAsync("В termux операция недоступна"); + + bool isEditManifest = false; + + if (IO.File.Exists("module/manifest.json")) + { + if (HttpContext.Request.Cookies.TryGetValue("passwd", out string passwd) && passwd == AppInit.rootPasswd) + { + isEditManifest = true; + } + else + { + HttpContext.Response.Redirect("/admin"); + return Task.CompletedTask; + } + } + + if (HttpContext.Request.Method == "POST") + { + var modules = new List(10); + + if (online == "on") + modules.Add("{\"enable\":true,\"dll\":\"Online.dll\"}"); + + if (sisi == "on") + modules.Add("{\"enable\":true,\"dll\":\"SISI.dll\"}"); + + if (!string.IsNullOrEmpty(jac)) + { + modules.Add("{\"enable\":true,\"initspace\":\"Jackett.ModInit\",\"dll\":\"JacRed.dll\"}"); + + #region JacRed.conf + if (jac == "fdb") + { + var jacPath = "module/JacRed.conf"; + + JObject jj; + if (IO.File.Exists(jacPath)) + { + string txt = IO.File.ReadAllText(jacPath).Trim(); + if (string.IsNullOrEmpty(txt)) + jj = new JObject(); + else + { + if (!txt.StartsWith("{")) + txt = "{" + txt + "}"; + + try + { + jj = JsonConvert.DeserializeObject(txt) ?? new JObject(); + } + catch + { + jj = new JObject(); + } + } + } + else + { + jj = new JObject(); + } + + jj["typesearch"] = "red"; + IO.File.WriteAllText(jacPath, JsonConvert.SerializeObject(jj, Formatting.Indented)); + } + #endregion + } + + if (dlna == "on") + modules.Add("{\"enable\":true,\"dll\":\"DLNA.dll\"}"); + + if (tracks == "on") + modules.Add("{\"enable\":true,\"initspace\":\"Tracks.ModInit\",\"dll\":\"Tracks.dll\"}"); + + if (ts == "on") + modules.Add("{\"enable\":true,\"initspace\":\"TorrServer.ModInit\",\"dll\":\"TorrServer.dll\"}"); + + if (catalog == "on") + modules.Add("{\"enable\":true,\"initspace\":\"Catalog.ModInit\",\"dll\":\"Catalog.dll\"}"); + + if (merch == "on") + modules.Add("{\"enable\":false,\"dll\":\"Merchant.dll\"}"); + + IO.File.WriteAllText("module/manifest.json", $"[{string.Join(",", modules)}]"); + + if (eng != "on") + UpdateInitConf(j => j["disableEng"] = true); + + if (isEditManifest) + { + return HttpContext.Response.WriteAsync("Перезагрузите lampac для изменения настроек"); + } + else + { + #region frontend cloudflare + if (HttpContext.Request.Headers.TryGetValue("CF-Connecting-IP", out var xip) && !string.IsNullOrEmpty(xip)) + { + UpdateInitConf(j => + { + var listen = j["listen"] as JObject; + if (listen == null) + { + listen = new JObject(); + j["listen"] = listen; + } + + listen["frontend"] = "cloudflare"; + }); + } + #endregion + + #region htmlSuccess + + #region shared_passwd + string shared_passwd = CrypTo.unic(8).ToLower(); + + UpdateInitConf(j => + { + var accsdb = j["accsdb"] as JObject; + if (accsdb == null) + { + accsdb = new JObject(); + j["accsdb"] = accsdb; + } + + accsdb["enable"] = true; + accsdb["shared_passwd"] = shared_passwd; + }); + + string sharedBlock = $@"
Авторизация в Lampa

+ Пароль: {shared_passwd} +

"; + #endregion + + string htmlSuccesds = $@" + + + + Настройка завершена + + + + + +

Настройка завершена

+ +{sharedBlock} + +
+ Админ панель

+ Aдрес: {host}/admin
+ Пароль: {IO.File.ReadAllText("passwd")} +
+ +
+ +
+
+ Media Station X

+ Settings -> Start Parameter -> Setup
+ Enter current ip address and port: {HttpContext.Request.Host.Value}

+ Убрать/Добавить адреса можно в /home/lampac/msx.json +
+
+ +
+ +
+ Виджет для Samsung

+ {host}/samsung.wgt +
+ +
+ +
+ Для android apk

+ Зажмите кнопку назад и введите новый адрес: {host} +
+ +
+ +
+ Плагины для Lampa

+ Заходим в настройки - расширения, жмем на кнопку ""добавить плагин"". В окне ввода вписываем адрес плагина {host}/on.js и перезагружаем виджет удерживая кнопку ""назад"" пока виджет не закроется. +
+ +
+ +
+ TorrServer (если установлен)

+ {host}/ts +
+ + +"; + + return HttpContext.Response.WriteAsync(htmlSuccesds).ContinueWith(t => Shared.Startup.appReload.Reload()); + #endregion + } + } + + #region renderHtml + string renderHtml() + { + var modules = IO.File.Exists("module/manifest.json") ? JsonConvert.DeserializeObject>(IO.File.ReadAllText("module/manifest.json")) : null; + + string IsChecked(string name, string def) + { + if (modules == null) + return def; + + bool res = modules.FirstOrDefault(m => m.dll == name)?.enable ?? false; + return res ? "checked" : string.Empty; + } + + return $@" + + + + + Модули + + + + + +
+
+ +
+ Онлайн балансеры Rezka, Filmix, etc +
+
+       ENG балансеры +
+
+ Клубничка 18+, PornHub, Xhamster, etc +
+
+ Альтернативные источники каталога cub и tmdb +
+
+ DLNA - Загрузка торрентов и просмотр медиа файлов с локального устройства +
+
+ TorrServer - возможность просматривать торренты в онлайн +
+
+ Tracks - транскодинг видео и замена названий аудиодорожек с rus1, rus2 на читаемые LostFilm, HDRezka, etc +
+
+ Автоматизация оплаты FreeKassa, Streampay, Litecoin, CryptoCloud +
+ +

+ +
+ Быстрый поиск по внешним базам JacRed, Rutor, Kinozal, NNM-Club, Rutracker, etc +
+
+ Локальный jacred.xyz (не рекомендуется ставить на домашние устройства) - 2GB HDD +
+
+ +
"; + } + #endregion + + return HttpContext.Response.WriteAsync(renderHtml()); + } + #endregion + + + #region UpdateInitConf + void UpdateInitConf(Action modify) + { + JObject jo; + + if (IO.File.Exists("init.conf")) + { + string initconf = IO.File.ReadAllText("init.conf").Trim(); + if (string.IsNullOrEmpty(initconf)) + jo = new JObject(); + + else + { + if (!initconf.StartsWith("{")) + initconf = "{" + initconf + "}"; + + try + { + jo = JsonConvert.DeserializeObject(initconf) ?? new JObject(); + } + catch + { + jo = new JObject(); + } + } + } + else + { + jo = new JObject(); + } + + modify?.Invoke(jo); + + IO.File.WriteAllText("init.conf", JsonConvert.SerializeObject(jo, Formatting.Indented)); + } + #endregion + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/BookmarkController.cs b/BaseModule/Controllers/BookmarkController.cs new file mode 100644 index 0000000..0890a05 --- /dev/null +++ b/BaseModule/Controllers/BookmarkController.cs @@ -0,0 +1,798 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Engine.Utilities; +using Shared.Models; +using Shared.Models.SQL; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; + +namespace Lampac.Controllers +{ + public class BookmarkController : BaseController + { + #region bookmark.js + [HttpGet] + [AllowAnonymous] + [Route("bookmark.js")] + [Route("bookmark/js/{token}")] + public ActionResult BookmarkJS(string token) + { + if (!AppInit.conf.sync_user.enable) + return Content(string.Empty, "application/javascript; charset=utf-8"); + + var sb = new StringBuilder(FileCache.ReadAllText("plugins/bookmark.js")); + + sb.Replace("{localhost}", host) + .Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + static readonly string[] BookmarkCategories = { + "history", + "like", + "watch", + "wath", + "book", + "look", + "viewed", + "scheduled", + "continued", + "thrown" + }; + + + #region List + [HttpGet] + [Route("/bookmark/list")] + public async Task List(string filed) + { + if (!AppInit.conf.sync_user.enable) + return ContentTo("{}"); + + string userUid = getUserid(requestInfo, HttpContext); + + #region migration storage to sql + if (AppInit.conf.sync_user.version != 1 && !string.IsNullOrEmpty(requestInfo.user_uid)) + { + string profile_id = getProfileid(requestInfo, HttpContext); + string id = requestInfo.user_uid + profile_id; + + string md5key = AppInit.conf.storage.md5name ? CrypTo.md5(id) : Regex.Replace(id, "[^a-z0-9\\-]", ""); + string storageFile = $"database/storage/sync_favorite/{md5key.Substring(0, 2)}/{md5key.Substring(2)}"; + + if (System.IO.File.Exists(storageFile) && !System.IO.File.Exists($"{storageFile}.migration")) + { + try + { + await SyncUserContext.semaphore.WaitAsync(TimeSpan.FromSeconds(40)); + + if (System.IO.File.Exists(storageFile) && !System.IO.File.Exists($"{storageFile}.migration")) + { + var content = System.IO.File.ReadAllText(storageFile); + if (!string.IsNullOrWhiteSpace(content)) + { + var root = JsonConvert.DeserializeObject(content); + + var favorite = (JObject)root["favorite"]; + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + var (entity, loaded) = LoadBookmarks(sqlDb, userUid, createIfMissing: true); + bool changed = false; + + EnsureDefaultArrays(loaded); + + #region migrate card objects + if (favorite["card"] is JArray srcCards) + { + foreach (var c in srcCards.Children()) + { + changed |= EnsureCard(loaded, c, c?["id"]?.ToString(), insert: false); + } + } + #endregion + + #region migrate categories + foreach (var prop in favorite.Properties()) + { + var name = prop.Name.ToLowerAndTrim(); + + if (string.Equals(name, "card", StringComparison.OrdinalIgnoreCase)) + continue; + + var srcValue = prop.Value; + + if (BookmarkCategories.Contains(name)) + { + if (srcValue is JArray srcArray) + { + var dest = GetCategoryArray(loaded, name); + foreach (var t in srcArray) + { + var idStr = t?.ToString(); + if (string.IsNullOrWhiteSpace(idStr)) + continue; + + if (dest.Any(dt => dt.ToString() == idStr) == false) + { + if (long.TryParse(idStr, out long _id) && _id > 0) + dest.Add(_id); + else + dest.Add(idStr); + + changed = true; + } + } + } + } + else + { + var existing = loaded[name]; + if (existing == null || !JToken.DeepEquals(existing, srcValue)) + { + loaded[name] = srcValue; + changed = true; + } + } + } + #endregion + + if (changed) + Save(sqlDb, entity, loaded); + } + + System.IO.File.Create($"{storageFile}.migration"); + } + } + } + catch { } + finally + { + SyncUserContext.semaphore.Release(); + } + } + } + #endregion + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + bool IsDbInitialization = sqlDb.bookmarks.AsNoTracking().FirstOrDefault(i => i.user == userUid) != null; + if (!IsDbInitialization) + return Json(new { dbInNotInitialization = true }); + + var data = GetBookmarksForResponse(sqlDb); + if (!string.IsNullOrEmpty(filed)) + return ContentTo(data[filed].ToString(Formatting.None)); + + return ContentTo(data.ToString(Formatting.None)); + } + } + #endregion + + #region Set + [HttpPost] + [Route("/bookmark/set")] + public async Task Set(string connectionId) + { + if (string.IsNullOrEmpty(requestInfo.user_uid) || !AppInit.conf.sync_user.enable) + return JsonFailure(); + + using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) + { + string body = await reader.ReadToEndAsync(); + if (string.IsNullOrWhiteSpace(body)) + return JsonFailure(); + + var token = JsonConvert.DeserializeObject(body); + if (token == null) + return JsonFailure(); + + var jobs = new List(); + if (token.Type == JTokenType.Array) + { + foreach (var obj in token.Children()) + jobs.Add(obj); + } + else if (token is JObject singleJob) + { + jobs.Add(singleJob); + } + + bool IsDbInitialization = false; + + try + { + await SyncUserContext.semaphore.WaitAsync(TimeSpan.FromSeconds(30)); + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + string userUid = getUserid(requestInfo, HttpContext); + + IsDbInitialization = sqlDb.bookmarks.AsNoTracking().FirstOrDefault(i => i.user == userUid) != null; + + var (entity, data) = LoadBookmarks(sqlDb, userUid, createIfMissing: true); + + foreach (var job in jobs) + { + string where = job.Value("where")?.ToLowerAndTrim(); + if (string.IsNullOrWhiteSpace(where)) + return JsonFailure(); + + if (IsDbInitialization && AppInit.conf.sync_user.fullset == false) + { + if (where == "card" || BookmarkCategories.Contains(where)) + return JsonFailure("enable sync_user.fullset in init.conf"); + } + + if (!job.TryGetValue("data", out var dataValue)) + return JsonFailure(); + + data[where] = dataValue; + } + + EnsureDefaultArrays(data); + + Save(sqlDb, entity, data); + } + } + catch + { + return JsonFailure(); + } + finally + { + SyncUserContext.semaphore.Release(); + } + + if (IsDbInitialization) + { + _ = Shared.Startup.Nws.EventsAsync(connectionId, requestInfo.user_uid, "bookmark", JsonConvertPool.SerializeObject(new + { + type = "set", + data = token, + profile_id = getProfileid(requestInfo, HttpContext) + })).ConfigureAwait(false); + } + + return JsonSuccess(); + } + } + + #endregion + + #region Add/Added + [HttpPost] + [Route("/bookmark/add")] + [Route("/bookmark/added")] + public async Task Add(string connectionId) + { + if (string.IsNullOrEmpty(requestInfo.user_uid) || !AppInit.conf.sync_user.enable) + return JsonFailure(); + + var readBody = await ReadPayloadAsync(); + + if (readBody.payloads.Count == 0) + return JsonFailure(); + + bool isAddedRequest = HttpContext?.Request?.Path.Value?.StartsWith("/bookmark/added", StringComparison.OrdinalIgnoreCase) == true; + + try + { + await SyncUserContext.semaphore.WaitAsync(TimeSpan.FromSeconds(30)); + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + var (entity, data) = LoadBookmarks(sqlDb, getUserid(requestInfo, HttpContext), createIfMissing: true); + bool changed = false; + + foreach (var payload in readBody.payloads) + { + var cardId = payload.ResolveCardId(); + if (cardId == null) + continue; + + changed |= EnsureCard(data, payload.Card, cardId); + + if (payload.Where != null) + changed |= AddToCategory(data, payload.Where, cardId); + + if (isAddedRequest) + changed |= MoveIdToFrontInAllCategories(data, cardId); + } + + if (changed) + { + Save(sqlDb, entity, data); + + if (readBody.token != null) + { + string edata = JsonConvertPool.SerializeObject(new + { + type = isAddedRequest ? "added" : "add", + profile_id = getProfileid(requestInfo, HttpContext), + data = readBody.token + }); + + _ = Shared.Startup.Nws.EventsAsync(connectionId, requestInfo.user_uid, "bookmark", edata).ConfigureAwait(false); + } + } + } + + return JsonSuccess(); + } + catch + { + return JsonFailure(); + } + finally + { + SyncUserContext.semaphore.Release(); + } + } + #endregion + + #region Remove + [HttpPost] + [Route("/bookmark/remove")] + public async Task Remove(string connectionId) + { + if (string.IsNullOrEmpty(requestInfo.user_uid) || !AppInit.conf.sync_user.enable) + return JsonFailure(); + + var readBody = await ReadPayloadAsync(); + + if (readBody.payloads.Count == 0) + return JsonFailure(); + + try + { + await SyncUserContext.semaphore.WaitAsync(TimeSpan.FromSeconds(30)); + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + var (entity, data) = LoadBookmarks(sqlDb, getUserid(requestInfo, HttpContext), createIfMissing: false); + if (entity == null) + return JsonSuccess(); + + bool changed = false; + + foreach (var payload in readBody.payloads) + { + var cardId = payload.ResolveCardId(); + if (cardId == null) + continue; + + if (payload.Where != null) + changed |= RemoveFromCategory(data, payload.Where, cardId); + + if (payload.Method == "card") + { + changed |= RemoveIdFromAllCategories(data, cardId); + changed |= RemoveCard(data, cardId); + } + } + + if (changed) + { + Save(sqlDb, entity, data); + + if (readBody.token != null) + { + string edata = JsonConvertPool.SerializeObject(new + { + type = "remove", + profile_id = getProfileid(requestInfo, HttpContext), + data = readBody.token + }); + + _ = Shared.Startup.Nws.EventsAsync(connectionId, requestInfo.user_uid, "bookmark", edata).ConfigureAwait(false); + } + } + } + + return JsonSuccess(); + } + catch + { + return JsonFailure(); + } + finally + { + SyncUserContext.semaphore.Release(); + } + } + #endregion + + + #region static + static string getUserid(RequestModel requestInfo, HttpContext httpContext) + { + string user_id = requestInfo.user_uid; + string profile_id = getProfileid(requestInfo, httpContext); + + if (!string.IsNullOrEmpty(profile_id)) + return $"{user_id}_{profile_id}"; + + return user_id; + } + + static string getProfileid(RequestModel requestInfo, HttpContext httpContext) + { + if (httpContext.Request.Query.TryGetValue("profile_id", out var profile_id) && !string.IsNullOrEmpty(profile_id) && profile_id != "0") + return profile_id; + + return string.Empty; + } + + JObject GetBookmarksForResponse(SyncUserContext sqlDb) + { + if (string.IsNullOrEmpty(requestInfo.user_uid)) + return CreateDefaultBookmarks(); + + string user_id = getUserid(requestInfo, HttpContext); + var entity = sqlDb.bookmarks.AsNoTracking().FirstOrDefault(i => i.user == user_id); + var data = entity != null ? DeserializeBookmarks(entity.data) : CreateDefaultBookmarks(); + EnsureDefaultArrays(data); + return data; + } + + static (SyncUserBookmarkSqlModel entity, JObject data) LoadBookmarks(SyncUserContext sqlDb, string userUid, bool createIfMissing) + { + JObject data = CreateDefaultBookmarks(); + SyncUserBookmarkSqlModel entity = null; + + if (!string.IsNullOrEmpty(userUid)) + { + entity = sqlDb.bookmarks.FirstOrDefault(i => i.user == userUid); + if (entity != null && !string.IsNullOrEmpty(entity.data)) + data = DeserializeBookmarks(entity.data); + } + + EnsureDefaultArrays(data); + + if (entity == null && createIfMissing && !string.IsNullOrEmpty(userUid)) + entity = new SyncUserBookmarkSqlModel { user = userUid }; + + return (entity, data); + } + + static JObject DeserializeBookmarks(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return CreateDefaultBookmarks(); + + try + { + var job = JsonConvert.DeserializeObject(json) ?? new JObject(); + EnsureDefaultArrays(job); + return job; + } + catch + { + return CreateDefaultBookmarks(); + } + } + + static JObject CreateDefaultBookmarks() + { + var obj = new JObject + { + ["card"] = new JArray() + }; + + foreach (var category in BookmarkCategories) + obj[category] = new JArray(); + + return obj; + } + + static void EnsureDefaultArrays(JObject root) + { + if (root == null) + return; + + if (root["card"] is not JArray) + root["card"] = new JArray(); + + foreach (var category in BookmarkCategories) + { + if (root[category] is not JArray) + root[category] = new JArray(); + } + } + + static bool EnsureCard(JObject data, JObject card, string idStr, bool insert = true) + { + if (data == null || card == null || string.IsNullOrWhiteSpace(idStr)) + return false; + + var cardArray = GetCardArray(data); + var newCard = (JObject)card.DeepClone(); + + foreach (var existing in cardArray.Children().ToList()) + { + var token = existing["id"]; + if (token != null && token.ToString() == idStr) + { + if (!JToken.DeepEquals(existing, newCard)) + { + existing.Replace(newCard); + return true; + } + + return false; + } + } + + if (insert) + cardArray.Insert(0, newCard); + else + cardArray.Add(newCard); + + return true; + } + + static bool AddToCategory(JObject data, string category, string idStr) + { + var array = GetCategoryArray(data, category); + + foreach (var token in array) + { + if (token.ToString() == idStr) + return false; + } + + if (long.TryParse(idStr, out long _id) && _id > 0) + array.Insert(0, _id); + else + array.Insert(0, idStr); + + return true; + } + + static bool MoveIdToFrontInAllCategories(JObject data, string idStr) + { + bool changed = false; + + foreach (var prop in data.Properties()) + { + if (string.Equals(prop.Name, "card", StringComparison.OrdinalIgnoreCase)) + continue; + + if (prop.Value is JArray array) + changed |= MoveIdToFront(array, idStr); + } + + return changed; + } + + static bool MoveIdToFront(JArray array, string idStr) + { + if (array == null) + return false; + + for (int i = 0; i < array.Count; i++) + { + var token = array[i]; + if (token?.ToString() == idStr) + { + if (i == 0) + return false; + + token.Remove(); + array.Insert(0, token); + return true; + } + } + + return false; + } + + static bool RemoveFromCategory(JObject data, string category, string idStr) + { + if (data[category] is not JArray array) + return false; + + return RemoveFromArray(array, idStr); + } + + static bool RemoveIdFromAllCategories(JObject data, string idStr) + { + bool changed = false; + + foreach (var property in data.Properties().ToList()) + { + if (property.Name == "card") + continue; + + if (property.Value is JArray array && RemoveFromArray(array, idStr)) + changed = true; + } + + return changed; + } + + static bool RemoveCard(JObject data, string idStr) + { + if (data["card"] is JArray cardArray) + { + foreach (var card in cardArray.Children().ToList()) + { + var token = card["id"]; + if (token != null && token.ToString() == idStr) + { + card.Remove(); + return true; + } + } + } + + return false; + } + + static JArray GetCardArray(JObject data) + { + if (data["card"] is JArray array) + return array; + + array = new JArray(); + data["card"] = array; + return array; + } + + static JArray GetCategoryArray(JObject data, string category) + { + if (data[category] is JArray array) + return array; + + array = new JArray(); + data[category] = array; + return array; + } + + static bool RemoveFromArray(JArray array, string idStr) + { + foreach (var token in array.ToList()) + { + if (token.ToString() == idStr) + { + token.Remove(); + return true; + } + } + + return false; + } + + static void Save(SyncUserContext sqlDb, SyncUserBookmarkSqlModel entity, JObject data) + { + if (entity == null) + return; + + entity.data = data.ToString(Formatting.None); + entity.updated = DateTime.UtcNow; + + if (entity.Id == 0) + sqlDb.bookmarks.Add(entity); + else + sqlDb.bookmarks.Update(entity); + + sqlDb.SaveChanges(); + } + + JsonResult JsonSuccess() => Json(new { success = true }); + + ActionResult JsonFailure(string message = null) => ContentTo(JsonConvertPool.SerializeObject(new { success = false, message })); + + async Task<(IReadOnlyList payloads, JToken token)> ReadPayloadAsync() + { + JToken token = null; + var payloads = new List(); + + using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: PoolInvk.bufferSize, leaveOpen: true)) + { + try + { + string json = await reader.ReadToEndAsync(); + + if (string.IsNullOrWhiteSpace(json)) + return (payloads, token); + + token = JsonConvert.DeserializeObject(json); + if (token == null) + return (payloads, token); + + if (token.Type == JTokenType.Array) + { + foreach (var obj in token.Children()) + payloads.Add(ParsePayload(obj)); + } + else if (token is JObject job) + { + payloads.Add(ParsePayload(job)); + } + } + catch { } + } + + return (payloads, token); + } + + static BookmarkEventPayload ParsePayload(JObject job) + { + var payload = new BookmarkEventPayload + { + Method = job.Value("method"), + CardIdRaw = job.Value("id") ?? job.Value("card_id") + }; + + payload.Where = (job.Value("where") ?? job.Value("list"))?.ToLowerAndTrim(); + if (string.IsNullOrEmpty(payload.Where) || payload.Where == "card") + payload.Where = null; + + if (job.TryGetValue("card", out var cardToken) && cardToken is JObject cardObj) + payload.Card = cardObj; + + return payload; + } + #endregion + + #region BookmarkEventPayload + sealed class BookmarkEventPayload + { + public string Method { get; set; } + + public string Where { get; set; } + + public JObject Card { get; set; } + + public string CardIdRaw { get; set; } + + public string ResolveCardId() + { + if (!string.IsNullOrWhiteSpace(CardIdRaw)) + return CardIdRaw.ToLowerAndTrim(); + + var token = Card?["id"]; + if (token != null) + { + if (token.Type == JTokenType.Integer) + return token.Value().ToString(); + + string _id = token.ToString(); + if (string.IsNullOrWhiteSpace(_id)) + return null; + + return _id.ToLowerAndTrim(); + } + + return null; + } + } + #endregion + } +} diff --git a/BaseModule/Controllers/ChromiumController.cs b/BaseModule/Controllers/ChromiumController.cs new file mode 100644 index 0000000..819da75 --- /dev/null +++ b/BaseModule/Controllers/ChromiumController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shared; + +namespace Lampac.Controllers +{ + public class ChromiumController : BaseController + { + [HttpGet] + [AllowAnonymous] + [Route("/api/chromium/ping")] + public string Ping() => "pong"; + + + [HttpGet] + [AllowAnonymous] + [Route("/api/chromium/iframe")] + public ActionResult RenderIframe(string src) + { + return ContentTo($@" + + + + + chromium iframe + + + + + "); + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/CmdController.cs b/BaseModule/Controllers/CmdController.cs new file mode 100644 index 0000000..fbc14e5 --- /dev/null +++ b/BaseModule/Controllers/CmdController.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Scripting; +using Shared; +using Shared.Engine; +using Shared.Models.CSharpGlobals; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Lampac.Controllers +{ + public class CmdController : BaseController + { + [HttpGet] + [Route("cmd/{key}/{*comand}")] + async public Task CMD(string key, string comand) + { + if (!AppInit.conf.cmd.TryGetValue(key, out var cmd)) + return; + + if (!string.IsNullOrEmpty(cmd.eval)) + { + var options = ScriptOptions.Default + .AddReferences(typeof(HttpRequest).Assembly).AddImports("Microsoft.AspNetCore.Http") + .AddReferences(typeof(Task).Assembly).AddImports("System.Threading.Tasks") + .AddReferences(CSharpEval.ReferenceFromFile("Newtonsoft.Json.dll")).AddImports("Newtonsoft.Json").AddImports("Newtonsoft.Json.Linq") + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")).AddImports("Shared.Engine").AddImports("Shared.Models") + .AddReferences(typeof(System.IO.File).Assembly).AddImports("System.IO") + .AddReferences(typeof(Process).Assembly).AddImports("System.Diagnostics"); + + var model = new CmdEvalModel(key, comand, requestInfo, HttpContext.Request, hybridCache, memoryCache); + + await CSharpEval.ExecuteAsync(cmd.eval, model, options); + } + else + { + if (cmd.arguments.Length == 0) + return; + + var _info = new ProcessStartInfo() + { + FileName = cmd.path + }; + + foreach (string a in cmd.arguments) + { + _info.ArgumentList.Add(a.Contains("{value}") + ? a.Replace("{value}", comand + HttpContext.Request.QueryString.Value) + : a + ); + } + + Process.Start(_info); + } + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/CorseuController.cs b/BaseModule/Controllers/CorseuController.cs new file mode 100644 index 0000000..dc10494 --- /dev/null +++ b/BaseModule/Controllers/CorseuController.cs @@ -0,0 +1,453 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Playwright; +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Base; +using Shared.PlaywrightCore; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Controllers +{ + public class CorseuController : BaseController + { + #region Routes + [HttpGet] + [AllowAnonymous] + [Route("/corseu/{token}/{*url}")] + public Task Get(string token, string url) + { + return ExecuteAsync(new CorseuRequest + { + url = url + HttpContext.Request.QueryString.Value, + auth_token = token + }); + } + + [HttpGet] + [AllowAnonymous] + [Route("/corseu")] + public Task Get(string auth_token, string method, string url, string data, string headers, string browser, int? httpversion, int? timeout, string encoding, bool? defaultHeaders, bool? autoredirect, string proxy, string proxy_name, bool? headersOnly) + { + return ExecuteAsync(new CorseuRequest + { + url = url, + method = method, + data = data, + browser = browser, + httpversion = httpversion, + timeout = timeout, + encoding = encoding, + defaultHeaders = defaultHeaders, + autoredirect = autoredirect, + proxy = proxy, + proxy_name = proxy_name, + headersOnly = headersOnly, + auth_token = auth_token, + headers = ParseHeaders(headers) + }); + } + + [HttpPost] + [AllowAnonymous] + [Route("/corseu")] + async public Task Post() + { + try + { + using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8, leaveOpen: true)) + { + string body = await reader.ReadToEndAsync().ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(body)) + return BadRequest("Empty body"); + + var model = JsonConvert.DeserializeObject(body); + if (model == null) + return BadRequest("Invalid body"); + + return await ExecuteAsync(model); + } + } + catch (JsonException) + { + return BadRequest("Invalid JSON"); + } + } + #endregion + + + #region Execute + async Task ExecuteAsync(CorseuRequest model) + { + var init = AppInit.conf.corseu; + + if (init?.tokens == null || init.tokens.Length == 0) + return StatusCode((int)HttpStatusCode.Forbidden); + + if (string.IsNullOrEmpty(model?.auth_token) || !init.tokens.Contains(model.auth_token)) + return StatusCode((int)HttpStatusCode.Forbidden); + + if (string.IsNullOrWhiteSpace(model?.url)) + return BadRequest("url is empty"); + + InvkEvent.CorseuRequest(model); + + string method = string.IsNullOrWhiteSpace(model.method) ? "GET" : model.method.ToUpperInvariant(); + string browser = string.IsNullOrWhiteSpace(model.browser) ? "http" : model.browser.ToLowerInvariant(); + + var headers = model.headers != null + ? new Dictionary(model.headers, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); + + bool useDefaultHeaders = model.defaultHeaders ?? true; + bool autoRedirect = model.autoredirect ?? true; + bool headersOnly = model.headersOnly ?? false; + int timeout = model.timeout.HasValue && model.timeout.Value > 5 ? model.timeout.Value : 15; + int httpVersion = model.httpversion ?? 1; + + #region rules + if (init?.rules != null) + { + foreach (var rule in init.rules) + { + if (rule?.headers == null || rule.headers.Count == 0) + continue; + + if (string.IsNullOrEmpty(rule.method) || string.IsNullOrEmpty(rule.url)) + continue; + + if (!string.Equals(rule.method, method, StringComparison.OrdinalIgnoreCase)) + continue; + + if (!Regex.IsMatch(model.url, rule.url, RegexOptions.IgnoreCase)) + continue; + + var ruleHeaders = new Dictionary(rule.headers, StringComparer.OrdinalIgnoreCase); + + if (rule.replace) + { + headers = ruleHeaders; + } + else + { + foreach (var pair in ruleHeaders) + headers[pair.Key] = pair.Value; + } + } + } + #endregion + + string contentType = null; + if (headers.TryGetValue("content-type", out string ct)) + { + contentType = ct; + headers.Remove("content-type"); + } + + if (headers.ContainsKey("content-length")) + headers.Remove("content-length"); + + if (browser is "chromium" or "playwright") + return await SendWithChromiumAsync(method, model.url, model.data, headers, contentType, timeout, autoRedirect, headersOnly, model.proxy, model.proxy_name); + + return await SendWithHttpClientAsync(method, model.url, model.data, headers, contentType, timeout, httpVersion, useDefaultHeaders, autoRedirect, headersOnly, model.proxy, model.proxy_name, model.encoding); + } + #endregion + + #region HttpClient + async Task SendWithHttpClientAsync( + string method, string url, string data, Dictionary headers, + string contentType, int timeout, int httpVersion, bool useDefaultHeaders, bool autoRedirect, bool headersOnly, string encodingName, + string proxyValue, string proxyName) + { + var proxyManager = CreateProxy(url, proxyValue, proxyName); + + try + { + var handler = Http.Handler(url, proxyManager.Get()); + handler.AllowAutoRedirect = autoRedirect; + + var client = FrendlyHttp.MessageClient(httpVersion == 2 ? "http2" : "base", handler); + + using (var request = new HttpRequestMessage(new HttpMethod(method), url)) + { + request.Version = httpVersion == 2 ? HttpVersion.Version20 : HttpVersion.Version11; + + if (!string.IsNullOrEmpty(data)) + { + var encoding = string.IsNullOrEmpty(encodingName) + ? Encoding.UTF8 + : Encoding.GetEncoding(encodingName); + + var content = new StringContent(data, encoding); + + if (!string.IsNullOrEmpty(contentType)) + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + + request.Content = content; + } + + var headersModel = headers.Count > 0 ? HeadersModel.Init(headers) : null; + Http.DefaultRequestHeaders(url, request, null, null, headersModel, useDefaultHeaders); + + if (InvkEvent.IsCorseuHttpRequest()) + InvkEvent.CorseuHttpRequest(method, url, request); + + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted)) + { + cts.CancelAfter(TimeSpan.FromSeconds(Math.Max(5, timeout))); + + using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false)) + { + proxyManager.Success(); + + await CopyResponseAsync(response, headersOnly).ConfigureAwait(false); + return new EmptyResult(); + } + } + } + } + catch (OperationCanceledException) + { + proxyManager.Refresh(); + return StatusCode((int)HttpStatusCode.RequestTimeout); + } + catch (Exception ex) + { + proxyManager.Refresh(); + return StatusCode((int)HttpStatusCode.BadGateway, ex.Message); + } + } + #endregion + + #region Chromium + async Task SendWithChromiumAsync( + string method, string url, string data, Dictionary headers, + string contentType, int timeout, bool autoRedirect, bool headersOnly, + string proxyValue, string proxyName) + { + var proxyManager = CreateProxy(url, proxyValue, proxyName); + var proxy = proxyManager.BaseGet(); + + try + { + if (PlaywrightBrowser.Status == PlaywrightStatus.disabled) + return StatusCode((int)HttpStatusCode.BadGateway, "PlaywrightStatus disabled"); + + var contextHeaders = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); + var requestHeaders = new Dictionary(headers, StringComparer.OrdinalIgnoreCase); + + if (!string.IsNullOrEmpty(contentType)) + { + if (!requestHeaders.ContainsKey("content-type")) + { + requestHeaders["content-type"] = contentType; + contextHeaders["content-type"] = contentType; + } + } + + var contextOptions = new APIRequestNewContextOptions + { + IgnoreHTTPSErrors = true, + ExtraHTTPHeaders = Http.NormalizeHeaders(contextHeaders), + Timeout = timeout * 1000 + }; + + if (requestHeaders.TryGetValue("user-agent", out string _useragent)) + contextOptions.UserAgent = _useragent; + + var requestOptions = new APIRequestContextOptions + { + Method = method, + Headers = requestHeaders, + Timeout = timeout * 1000 + }; + + if (!string.IsNullOrEmpty(data)) + requestOptions.DataString = data; + + if (!autoRedirect) + requestOptions.MaxRedirects = 0; + + if (proxy.proxy != null) + { + contextOptions.Proxy = new Proxy + { + Server = proxy.data.ip, + Username = proxy.data.username, + Password = proxy.data.password + }; + } + + if (InvkEvent.IsCorseuPlaywrightRequest()) + InvkEvent.CorseuPlaywrightRequest(method, url, contextOptions, requestOptions); + + await using (var requestContext = await Chromium.playwright.APIRequest.NewContextAsync(contextOptions).ConfigureAwait(false)) + { + var response = await requestContext.FetchAsync(url, requestOptions).ConfigureAwait(false); + + try + { + HttpContext.Response.StatusCode = response.Status; + + foreach (var header in response.HeadersArray) + { + var headerName = header.Name.ToLowerInvariant(); + + if (ShouldSkipHeader(headerName)) + continue; + + if (headerName == "content-type") + HttpContext.Response.ContentType = header.Value; + + HttpContext.Response.Headers[header.Name] = header.Value; + } + + if (headersOnly) + { + proxyManager.Success(); + await HttpContext.Response.CompleteAsync().ConfigureAwait(false); + return new EmptyResult(); + } + + var body = await response.BodyAsync().ConfigureAwait(false); + if (body?.Length > 0) + await HttpContext.Response.Body.WriteAsync(body, 0, body.Length, HttpContext.RequestAborted).ConfigureAwait(false); + + proxyManager.Success(); + return new EmptyResult(); + } + finally + { + await response.DisposeAsync().ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + proxyManager.Refresh(); + return StatusCode((int)HttpStatusCode.RequestTimeout); + } + catch (Exception ex) + { + proxyManager.Refresh(); + return StatusCode((int)HttpStatusCode.BadGateway, ex.Message); + } + } + #endregion + + #region Helpers + Dictionary ParseHeaders(string headers) + { + try + { + if (!string.IsNullOrEmpty(headers)) + return JsonConvert.DeserializeObject>(headers); + } + catch { } + + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + ProxyManager CreateProxy(string url, string proxyValue, string proxyName) + { + var model = new BaseSettings() + { + plugin = $"corseu:{Regex.Match(url, "https?://([^/]+)")}" + }; + + if (!string.IsNullOrEmpty(proxyValue)) + { + model.proxy = new ProxySettings(); + model.proxy.list = [proxyValue]; + } + else if (!string.IsNullOrEmpty(proxyName)) + { + if (AppInit.conf.globalproxy != null) + { + var settings = AppInit.conf.globalproxy.FirstOrDefault(i => i.name == proxyName); + if (settings?.list != null && settings.list.Length > 0) + model.proxy = settings; + } + } + + if (model.proxy != null) + model.useproxy = true; + + return new ProxyManager("corseu", model); + } + + async Task CopyResponseAsync(HttpResponseMessage response, bool headersOnly) + { + var httpResponse = HttpContext.Response; + httpResponse.StatusCode = (int)response.StatusCode; + + foreach (var header in response.Headers) + { + if (ShouldSkipHeader(header.Key)) + continue; + + httpResponse.Headers[header.Key] = string.Join(", ", header.Value); + } + + foreach (var header in response.Content.Headers) + { + if (string.Equals(header.Key, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + httpResponse.ContentType = response.Content.Headers.ContentType?.ToString(); + continue; + } + + if (ShouldSkipHeader(header.Key)) + continue; + + httpResponse.Headers[header.Key] = string.Join(", ", header.Value); + } + + if (headersOnly) + { + await httpResponse.CompleteAsync().ConfigureAwait(false); + return; + } + + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + await responseStream.CopyToAsync(httpResponse.Body, HttpContext.RequestAborted).ConfigureAwait(false); + } + + bool ShouldSkipHeader(string header) + { + string key = header.ToLowerInvariant(); + + return key switch + { + "content-length" => true, + "transfer-encoding" => true, + "connection" => true, + "keep-alive" => true, + "content-disposition" => true, + "content-encoding" => true, + "content-security-policy" => true, + "vary" => true, + "alt-svc" => true, + _ when key.StartsWith("access-control") => true, + _ when key.StartsWith("x-") => true, + _ => false + }; + } + #endregion + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/CubController.cs b/BaseModule/Controllers/CubController.cs new file mode 100644 index 0000000..892e2e0 --- /dev/null +++ b/BaseModule/Controllers/CubController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shared; +using Shared.Engine; +using System.Web; + +namespace Lampac.Controllers +{ + public class CubController : BaseController + { + [HttpGet] + [AllowAnonymous] + [Route("cubproxy.js")] + [Route("cubproxy/js/{token}")] + public ActionResult CubProxy(string token) + { + if (!AppInit.conf.cub.enabled(requestInfo.Country)) + return Content(string.Empty, contentType: "application/javascript; charset=utf-8"); + + string file = FileCache.ReadAllText("plugins/cubproxy.js").Replace("{localhost}", host); + file = file.Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(file, contentType: "application/javascript; charset=utf-8"); + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/ErrorDocController.cs b/BaseModule/Controllers/ErrorDocController.cs new file mode 100644 index 0000000..9ec1af0 --- /dev/null +++ b/BaseModule/Controllers/ErrorDocController.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shared; +using Shared.Engine; + +namespace Lampac.Controllers +{ + public class ErrorDocController : BaseController + { + [HttpGet] + [AllowAnonymous] + [Route("/e/acb")] + public ActionResult Accsdb() + { + string shared_passwd = CrypTo.unic(8).ToLowerInvariant(); + string pw1 = CrypTo.unic(6).ToLowerInvariant(); + string pw2 = CrypTo.unic(8).ToLowerInvariant(); + + return ContentTo($@" + + + + + Настройка AccsDB + + + +
+
+
+

Добавьте в init.conf заменив email/unic_id на свои:

+
""accsdb"": {{
+  ""accounts"": {{
+    ""{pw1}@mail.ru"": ""2040-10-17T00:00:00"", // email cub.red
+    ""{pw2}"": ""2040-10-17T00:00:00"", // unic_id
+  }}
+}}
+
+

Или через {host}/admin > Пользователи > Добавить пользователя > В ID указать email/unic_id

+
+
+
+
+
+
+

Если нужно разрешить внешний доступ без добавления каждого устройства, создайте пароль доступа:

+
""accsdb"": {{
+  ""shared_passwd"": ""{shared_passwd}""
+}}
+
+

Так все кому вы сообщили пароль {shared_passwd} cмогут самостоятельно авторизоваться

+
+
+
+
+
+
+

Персональные пароли для плагинов, пример пароля kitty:

+
""accsdb"": {{
+  ""accounts"": {{
+    ""kitty"": ""2040-10-17T00:00:00"", 
+  }}
+}}
+
+

Или через {host}/admin > Пользователи > Добавить пользователя > В ID указать kitty

+ +
+

Все плагины сразу +
+ http://IP:9118/on/js/kitty +

+ +

Онлайн +
+ http://IP:9118/online/js/kitty +

+ +

Клубничка +
+ http://IP:9118/sisi/js/kitty +

+ +

DLNA +
+ http://IP:9118/dlna/js/kitty +

+ +

Таймкоды +
+ http://IP:9118/timecode/js/kitty +

+ +

Tracks и Транскодинг +
+ http://IP:9118/tracks/js/kitty +

+ +

TorrServer +
+ http://IP:9118/ts/js/kitty +

+
+
+
+ +"); + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/LampaWebController.cs b/BaseModule/Controllers/LampaWebController.cs new file mode 100644 index 0000000..9a1e168 --- /dev/null +++ b/BaseModule/Controllers/LampaWebController.cs @@ -0,0 +1,626 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Models.Events; +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; +using IO = System.IO; + +namespace Lampac.Controllers +{ + public class LampaWebController : BaseController + { + [HttpGet] + [AllowAnonymous] + [Route("/personal.lampa")] + [Route("/lampa-main/personal.lampa")] + [Route("/{myfolder}/personal.lampa")] + public ActionResult PersonalLampa(string myfolder) => StatusCode(200); + + + #region Index + [HttpGet] + [AllowAnonymous] + [Route("/")] + public ActionResult Index() + { + if (string.IsNullOrWhiteSpace(AppInit.conf.LampaWeb.index)) + return Content("api work", contentType: "text/plain; charset=utf-8"); + + if (AppInit.conf.LampaWeb.basetag && Regex.IsMatch(AppInit.conf.LampaWeb.index, "/[^\\./]+\\.html$")) + { + if (!memoryCache.TryGetValue($"LampaWeb.index:{AppInit.conf.LampaWeb.index}", out string html)) + { + html = IO.File.ReadAllText($"wwwroot/{AppInit.conf.LampaWeb.index}"); + html = html.Replace("", $""); + + memoryCache.Set($"LampaWeb.index:{AppInit.conf.LampaWeb.index}", html, DateTime.Now.AddMinutes(1)); + } + + return Content(html, contentType: "text/html; charset=utf-8"); + } + + return LocalRedirect($"/{AppInit.conf.LampaWeb.index}"); + } + #endregion + + #region Extensions + [HttpGet] + [AllowAnonymous] + [Route("/extensions")] + public ActionResult Extensions() + { + return ContentTo(FileCache.ReadAllText("plugins/extensions.json").Replace("{localhost}", host).Replace("\n", "").Replace("\r", "")); + } + #endregion + + #region testaccsdb + [HttpGet] + [HttpPost] + [Route("/testaccsdb")] + public ActionResult TestAccsdb(string account_email, string uid) + { + if (!string.IsNullOrEmpty(AppInit.conf.accsdb.shared_passwd) && uid == AppInit.conf.accsdb.shared_passwd) + return ContentTo("{\"accsdb\": true, \"newuid\": true}"); + + if (!string.IsNullOrEmpty(uid) && !string.IsNullOrEmpty(account_email) && account_email == AppInit.conf.accsdb.shared_passwd) + { + try + { + string file = "users.json"; + + JArray arr = new JArray(); + + if (IO.File.Exists(file)) + { + var txt = IO.File.ReadAllText(file); + if (!string.IsNullOrWhiteSpace(txt)) + try { arr = JArray.Parse(txt); } catch { arr = new JArray(); } + } + + bool exists = arr.Children().Any(o => + (o.Value("id") != null && o.Value("id").Equals(uid, StringComparison.OrdinalIgnoreCase)) || + (o["ids"] != null && o["ids"].Any(t => t.ToString().Equals(uid, StringComparison.OrdinalIgnoreCase))) + ); + + if (exists) + return ContentTo("{\"accsdb\": false}"); + + var obj = new JObject(); + obj["id"] = uid; + obj["expires"] = DateTime.Now.AddDays(Math.Max(1, AppInit.conf.accsdb.shared_daytime)); + + arr.Add(obj); + + IO.File.WriteAllText(file, arr.ToString(Formatting.Indented)); + + return ContentTo("{\"accsdb\": false, \"success\": true, \"uid\": \"" + uid + "\"}"); + } + catch { return ContentTo("{\"accsdb\": true}"); } + } + + return ContentTo("{\"accsdb\": false, \"success\": true}"); + } + #endregion + + + #region app.min.js + [HttpGet] + [AllowAnonymous] + [Route("/app.min.js")] + [Route("{type}/app.min.js")] + public ContentResult LampaApp(string type) + { + if (string.IsNullOrEmpty(type)) + { + if (AppInit.conf.LampaWeb.path != null) + { + type = AppInit.conf.LampaWeb.path; + } + else + { + if (AppInit.conf.LampaWeb.index == null || !AppInit.conf.LampaWeb.index.Contains("/")) + return Content(string.Empty, "application/javascript; charset=utf-8"); + + type = AppInit.conf.LampaWeb.index.Split("/")[0]; + } + } + else + { + type = Regex.Replace(type, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase); + } + + bool usecubproxy = AppInit.conf.cub.enabled(requestInfo.Country); + var apr = AppInit.conf.LampaWeb.appReplace ?? InvkEvent.conf?.Controller?.AppReplace?.appjs?.regex; + + string memKey = $"ApiController:{type}:{host}:{usecubproxy}:{apr?.Count ?? 0}:app.min.js"; + if (!memoryCache.TryGetValue(memKey, out string file)) + { + file = IO.File.ReadAllText($"wwwroot/{type}/app.min.js"); + + #region appReplace + if (apr != null) + { + foreach (var r in apr) + { + string val = r.Value; + if (val.StartsWith("file:")) + val = IO.File.ReadAllText(val.Substring(5)); + + val = val.Replace("{localhost}", host).Replace("{host}", Regex.Replace(host, "^https?://", "")); + file = Regex.Replace(file, r.Key, val, RegexOptions.IgnoreCase); + } + } + + if (InvkEvent.conf?.Controller?.AppReplace?.appjs?.list != null) + { + foreach (var r in InvkEvent.conf.Controller.AppReplace.appjs.list) + { + string val = r.Value; + if (val.StartsWith("file:")) + val = IO.File.ReadAllText(val.Substring(5)); + + val = val.Replace("{localhost}", host).Replace("{host}", Regex.Replace(host, "^https?://", "")); + file = file.Replace(r.Key, val); + } + } + #endregion + + string playerinner = FileCache.ReadAllText("plugins/player-inner.js", saveCache: false) + .Replace("{useplayer}", (!string.IsNullOrEmpty(AppInit.conf.playerInner)).ToString().ToLower()) + .Replace("{notUseTranscoding}", (AppInit.conf.transcoding.enable == false).ToString().ToLower()); + + var bulder = new StringBuilder(file); + + bulder = bulder.Replace("Player.play(element);", playerinner); + + if (usecubproxy) + { + bulder = bulder.Replace("protocol + mirror + '/api/checker'", $"'{host}/cub/api/checker'"); + bulder = bulder.Replace("Utils$1.protocol() + 'tmdb.' + object$2.cub_domain + '/' + u,", $"'{host}/cub/tmdb./' + u,"); + bulder = bulder.Replace("Utils$2.protocol() + 'tmdb.' + object$2.cub_domain + '/' + u,", $"'{host}/cub/tmdb./' + u,"); + bulder = bulder.Replace("Utils$1.protocol() + object$2.cub_domain", $"'{host}/cub/red'"); + bulder = bulder.Replace("Utils$2.protocol() + object$2.cub_domain", $"'{host}/cub/red'"); + bulder = bulder.Replace("object$2.cub_domain", $"'{AppInit.conf.cub.mirror}'"); + } + + bulder = bulder.Replace("http://lite.lampa.mx", $"{host}/{type}"); + bulder = bulder.Replace("https://yumata.github.io/lampa-lite", $"{host}/{type}"); + + bulder = bulder.Replace("http://lampa.mx", $"{host}/{type}"); + bulder = bulder.Replace("https://yumata.github.io/lampa", $"{host}/{type}"); + + bulder = bulder.Replace("window.lampa_settings.dcma = dcma;", "window.lampa_settings.fixdcma = true;"); + bulder = bulder.Replace("Storage.get('vpn_checked_ready', 'false')", "true"); + + bulder = bulder.Replace("status$1 = false;", "status$1 = true;"); // local apk to personal.lampa + bulder = bulder.Replace("return status$1;", "return true;"); // отключение рекламы + bulder = bulder.Replace("if (!Storage.get('metric_uid', ''))", "return;"); // metric + bulder = bulder.Replace("function log(data) {", "function log(data) { return;"); + bulder = bulder.Replace("function stat$1(method, name) {", "function stat$1(method, name) { return;"); + bulder = bulder.Replace("if (domain) {", "if (false) {"); + + bulder = bulder.Replace("{localhost}", host); + + file = bulder.ToString(); + + if (AppInit.conf.mikrotik == false) + memoryCache.Set(memKey, file, DateTime.Now.AddMinutes(1)); + } + + if (InvkEvent.conf?.Controller?.AppReplace?.appjs?.eval != null) + file = InvkEvent.AppReplace("appjs", new EventAppReplace(file, null, type, host, requestInfo, HttpContext.Request, hybridCache)); + + return Content(file, "application/javascript; charset=utf-8"); + } + #endregion + + #region app.css + [HttpGet] + [AllowAnonymous] + [Route("/css/app.css")] + [Route("{type}/css/app.css")] + public ContentResult LampaAppCss(string type) + { + if (string.IsNullOrEmpty(type)) + { + if (AppInit.conf.LampaWeb.path != null) + { + type = AppInit.conf.LampaWeb.path; + } + else + { + if (AppInit.conf.LampaWeb.index == null || !AppInit.conf.LampaWeb.index.Contains("/")) + return Content(string.Empty, "application/javascript; charset=utf-8"); + + type = AppInit.conf.LampaWeb.index.Split("/")[0]; + } + } + else + { + type = Regex.Replace(type, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase); + } + + var apr = AppInit.conf.LampaWeb.cssReplace ?? InvkEvent.conf?.Controller?.AppReplace?.appcss?.regex; + + string memKey = $"ApiController:css/app.css:{type}:{host}:{apr?.Count ?? 0}"; + if (!memoryCache.TryGetValue(memKey, out string css)) + { + css = IO.File.ReadAllText($"wwwroot/{type}/css/app.css"); + + #region appReplace + if (apr != null) + { + foreach (var r in apr) + { + string val = r.Value; + if (val.StartsWith("file:")) + val = IO.File.ReadAllText(val.Substring(5)); + + val = val.Replace("{localhost}", host).Replace("{host}", Regex.Replace(host, "^https?://", "")); + css = Regex.Replace(css, r.Key, val, RegexOptions.IgnoreCase); + } + } + + if (InvkEvent.conf?.Controller?.AppReplace?.appcss?.list != null) + { + foreach (var r in InvkEvent.conf.Controller.AppReplace.appcss.list) + { + string val = r.Value; + if (val.StartsWith("file:")) + val = IO.File.ReadAllText(val.Substring(5)); + + val = val.Replace("{localhost}", host).Replace("{host}", Regex.Replace(host, "^https?://", "")); + css = css.Replace(r.Key, val); + } + } + #endregion + + memoryCache.Set(memKey, css, DateTime.Now.AddMinutes(AppInit.conf.multiaccess ? 5 : 1)); + } + + if (InvkEvent.conf?.Controller?.AppReplace?.appcss?.eval != null) + css = InvkEvent.AppReplace("appcss", new EventAppReplace(css, null, type, host, requestInfo, HttpContext.Request, hybridCache)); + + return Content(css, "text/css; charset=utf-8"); + } + #endregion + + + #region samsung.wgt + [HttpGet] + [AllowAnonymous] + [Route("samsung.wgt")] + public ActionResult SamsWgt(string overwritehost) + { + string folder = "data/widgets"; + + if (!IO.File.Exists($"{folder}/samsung/loader.js")) + return Content(string.Empty); + + string wgt = $"{folder}/{CrypTo.md5(overwritehost ?? host + "v3")}.wgt"; + if (IO.File.Exists(wgt)) + return File(IO.File.OpenRead(wgt), "application/octet-stream"); + + string index = IO.File.ReadAllText($"{folder}/samsung/index.html"); + IO.File.WriteAllText($"{folder}/samsung/publish/index.html", index.Replace("{localhost}", overwritehost ?? host)); + + string loader = IO.File.ReadAllText($"{folder}/samsung/loader.js"); + IO.File.WriteAllText($"{folder}/samsung/publish/loader.js", loader.Replace("{localhost}", overwritehost ?? host)); + + string app = IO.File.ReadAllText($"{folder}/samsung/app.js"); + IO.File.WriteAllText($"{folder}/samsung/publish/app.js", app.Replace("{localhost}", overwritehost ?? host)); + + IO.File.Copy($"{folder}/samsung/icon.png", $"{folder}/samsung/publish/icon.png", overwrite: true); + IO.File.Copy($"{folder}/samsung/logo_appname_fg.png", $"{folder}/samsung/publish/logo_appname_fg.png", overwrite: true); + IO.File.Copy($"{folder}/samsung/config.xml", $"{folder}/samsung/publish/config.xml", overwrite: true); + + string gethash(string file) + { + using (SHA512 sha = SHA512.Create()) + { + return Convert.ToBase64String(sha.ComputeHash(IO.File.ReadAllBytes(file))); + //digestValue = hash.Remove(76) + "\n" + hash.Remove(0, 76); + } + } + + string indexhashsha512 = gethash($"{folder}/samsung/publish/index.html"); + string loaderhashsha512 = gethash($"{folder}/samsung/publish/loader.js"); + string apphashsha512 = gethash($"{folder}/samsung/publish/app.js"); + string confighashsha512 = gethash($"{folder}/samsung/publish/config.xml"); + string iconhashsha512 = gethash($"{folder}/samsung/publish/icon.png"); + string logohashsha512 = gethash($"{folder}/samsung/publish/logo_appname_fg.png"); + + string author_sigxml = IO.File.ReadAllText($"{folder}/samsung/author-signature.xml"); + author_sigxml = author_sigxml.Replace("loaderhashsha512", loaderhashsha512).Replace("apphashsha512", apphashsha512) + .Replace("iconhashsha512", iconhashsha512).Replace("logohashsha512", logohashsha512) + .Replace("confighashsha512", confighashsha512) + .Replace("indexhashsha512", indexhashsha512); + IO.File.WriteAllText($"{folder}/samsung/publish/author-signature.xml", author_sigxml); + + string authorsignaturehashsha512 = gethash($"{folder}/samsung/publish/author-signature.xml"); + string sigxml1 = IO.File.ReadAllText($"{folder}/samsung/signature1.xml"); + sigxml1 = sigxml1.Replace("loaderhashsha512", loaderhashsha512).Replace("apphashsha512", apphashsha512) + .Replace("confighashsha512", confighashsha512).Replace("authorsignaturehashsha512", authorsignaturehashsha512) + .Replace("iconhashsha512", iconhashsha512).Replace("logohashsha512", logohashsha512).Replace("indexhashsha512", indexhashsha512); + IO.File.WriteAllText($"{folder}/samsung/publish/signature1.xml", sigxml1); + + ZipFile.CreateFromDirectory($"{folder}/samsung/publish/", wgt); + + return File(IO.File.OpenRead(wgt), "application/octet-stream"); + } + #endregion + + #region MSX + [HttpGet] + [AllowAnonymous] + [Route("msx/start.json")] + public ActionResult MSX() + { + return Content(FileCache.ReadAllText("msx.json").Replace("{localhost}", host), "application/json; charset=utf-8"); + } + #endregion + + #region startpage.js + [HttpGet] + [AllowAnonymous] + [Route("startpage.js")] + public ActionResult StartPage() + { + return Content(FileCache.ReadAllText("plugins/startpage.js").Replace("{localhost}", host), "application/javascript; charset=utf-8"); + } + #endregion + + #region lampainit.js + [HttpGet] + [AllowAnonymous] + [Route("lampainit.js")] + public ActionResult LamInit(bool lite) + { + string initiale = string.Empty; + var sb = new StringBuilder(FileCache.ReadAllText($"plugins/{(lite ? "liteinit" : "lampainit")}.js")); + + if (AppInit.modules != null) + { + if (lite) + { + if (AppInit.conf.LampaWeb.initPlugins.online && AppInit.modules.FirstOrDefault(i => i.dll == "Online.dll" && i.enable) != null) + initiale += "\"{localhost}/lite.js\","; + + if (AppInit.conf.LampaWeb.initPlugins.sisi && AppInit.modules.FirstOrDefault(i => i.dll == "SISI.dll" && i.enable) != null) + initiale += "\"{localhost}/sisi.js?lite=true\","; + + if (AppInit.conf.LampaWeb.initPlugins.sync) + initiale += "\"{localhost}/sync.js?lite=true\","; + } + else + { + if (AppInit.conf.LampaWeb.initPlugins.dlna && AppInit.modules.FirstOrDefault(i => i.dll == "DLNA.dll" && i.enable) != null) + initiale += "{\"url\": \"{localhost}/dlna.js\",\"status\": 1,\"name\": \"DLNA\",\"author\": \"lampac\"},"; + + if (AppInit.modules.FirstOrDefault(i => i.dll == "Tracks.dll" && i.enable) != null) + { + if (AppInit.conf.LampaWeb.initPlugins.tracks) + initiale += "{\"url\": \"{localhost}/tracks.js\",\"status\": 1,\"name\": \"Tracks.js\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.transcoding && AppInit.conf.transcoding.enable) + initiale += "{\"url\": \"{localhost}/transcoding.js\",\"status\": 1,\"name\": \"Transcoding video\",\"author\": \"lampac\"},"; + } + + if (AppInit.conf.LampaWeb.initPlugins.tmdbProxy) + initiale += "{\"url\": \"{localhost}/tmdbproxy.js\",\"status\": 1,\"name\": \"TMDB Proxy\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.online && AppInit.modules.FirstOrDefault(i => i.dll == "Online.dll" && i.enable) != null) + initiale += "{\"url\": \"{localhost}/online.js\",\"status\": 1,\"name\": \"Онлайн\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.catalog && AppInit.modules.FirstOrDefault(i => i.dll == "Catalog.dll" && i.enable) != null) + initiale += "{\"url\": \"{localhost}/catalog.js\",\"status\": 1,\"name\": \"Альтернативные источники каталога\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.sisi && AppInit.modules.FirstOrDefault(i => i.dll == "SISI.dll" && i.enable) != null) + { + initiale += "{\"url\": \"{localhost}/sisi.js\",\"status\": 1,\"name\": \"Клубничка\",\"author\": \"lampac\"},"; + initiale += "{\"url\": \"{localhost}/startpage.js\",\"status\": 1,\"name\": \"Стартовая страница\",\"author\": \"lampac\"},"; + } + + if (AppInit.conf.LampaWeb.initPlugins.sync) + initiale += "{\"url\": \"{localhost}/sync.js\",\"status\": 1,\"name\": \"Синхронизация\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.timecode) + initiale += "{\"url\": \"{localhost}/timecode.js\",\"status\": 1,\"name\": \"Синхронизация тайм-кодов\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.bookmark) + initiale += "{\"url\": \"{localhost}/bookmark.js\",\"status\": 1,\"name\": \"Синхронизация закладок\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.torrserver && AppInit.modules.FirstOrDefault(i => i.dll == "TorrServer.dll" && i.enable) != null) + initiale += "{\"url\": \"{localhost}/ts.js\",\"status\": 1,\"name\": \"TorrServer\",\"author\": \"lampac\"},"; + + if (AppInit.conf.LampaWeb.initPlugins.backup) + initiale += "{\"url\": \"{localhost}/backup.js\",\"status\": 1,\"name\": \"Backup\",\"author\": \"lampac\"},"; + + if (AppInit.conf.pirate_store) + sb = sb.Replace("{pirate_store}", FileCache.ReadAllText("plugins/pirate_store.js")); + + if (AppInit.conf.accsdb.enable || (!requestInfo.IsLocalIp && !AppInit.conf.WAF.allowExternalIpAccess)) + sb = sb.Replace("{deny}", FileCache.ReadAllText("plugins/deny.js").Replace("{cubMesage}", AppInit.conf.accsdb.authMesage)); + } + } + + sb = sb.Replace("{lampainit-invc}", FileCache.ReadAllText("plugins/lampainit-invc.js")); + sb = sb.Replace("{initiale}", Regex.Replace(initiale, ",$", "")); + + sb = sb.Replace("{country}", requestInfo.Country); + sb = sb.Replace("{localhost}", host); + sb = sb.Replace("{deny}", string.Empty); + sb = sb.Replace("{pirate_store}", string.Empty); + + sb = sb.Replace("{ major: 0, minor: 0 }", $"{{major: {appversion}, minor: {minorversion}}}"); + + if (AppInit.modules != null && AppInit.modules.FirstOrDefault(i => i.dll == "JacRed.dll" && i.enable) != null) + sb = sb.Replace("{jachost}", Regex.Replace(host, "^https?://", "")); + else + sb = sb.Replace("{jachost}", "redapi.apn.monster"); + + #region full_btn_priority_hash + string online_version = Regex.Match(FileCache.ReadAllText("plugins/online.js"), "version: '([^']+)'").Groups[1].Value; + + string LampaUtilshash(string input) + { + if (!AppInit.conf.online.version) + input = input.Replace($"v{online_version}", ""); + + string str = (input ?? string.Empty); + int hash = 0; + + if (str.Length == 0) return hash.ToString(); + + for (int i = 0; i < str.Length; i++) + { + int _char = str[i]; + + hash = (hash << 5) - hash + _char; + hash = hash & hash; // Преобразование в 32-битное целое число + } + + return Math.Abs(hash).ToString(); + } + + string full_btn_priority_hash = LampaUtilshash($"
\n \n \n \n \n \n \n\n Онлайн\n
"); + + sb = sb.Replace("{full_btn_priority_hash}", full_btn_priority_hash) + .Replace("{btn_priority_forced}", AppInit.conf.online.btn_priority_forced.ToString().ToLower()); + #endregion + + #region domain token + if (!string.IsNullOrEmpty(AppInit.conf.accsdb.domainId_pattern)) + { + string token = Regex.Match(HttpContext.Request.Host.Host, AppInit.conf.accsdb.domainId_pattern).Groups[1].Value; + sb = sb.Replace("{token}", token); + } + else { sb = sb.Replace("{token}", string.Empty); } + #endregion + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + #region on.js + [HttpGet] + [AllowAnonymous] + [Route("on.js")] + [Route("on/js/{token}")] + [Route("on/h/{token}")] + [Route("on/{token}")] + public ActionResult LamOnInit(string token, bool adult = true) + { + if (adult && HttpContext.Request.Path.Value.StartsWith("/on/h/")) + adult = false; + + var plugins = new List(10); + var sb = new StringBuilder(FileCache.ReadAllText("plugins/on.js")); + + if (AppInit.modules != null) + { + void send(string name, bool worktoken) + { + if (worktoken && !string.IsNullOrEmpty(token)) + { + plugins.Add($"\"{{localhost}}/{name}/js/{HttpUtility.UrlEncode(token)}\""); + } + else + { + plugins.Add($"\"{{localhost}}/{name}.js\""); + } + } + + if (AppInit.conf.LampaWeb.initPlugins.dlna && AppInit.modules.FirstOrDefault(i => i.dll == "DLNA.dll" && i.enable) != null) + send("dlna", true); + + if (AppInit.modules.FirstOrDefault(i => i.dll == "Tracks.dll" && i.enable) != null) + { + if (AppInit.conf.LampaWeb.initPlugins.tracks) + send("tracks", true); + + if (AppInit.conf.LampaWeb.initPlugins.transcoding && AppInit.conf.transcoding.enable) + send("transcoding", true); + } + + if (AppInit.conf.LampaWeb.initPlugins.tmdbProxy) + send("tmdbproxy", true); + + if (AppInit.conf.LampaWeb.initPlugins.online && AppInit.modules.FirstOrDefault(i => i.dll == "Online.dll" && i.enable) != null) + send("online", true); + + if (adult) + { + if (AppInit.conf.LampaWeb.initPlugins.sisi && AppInit.modules.FirstOrDefault(i => i.dll == "SISI.dll" && i.enable) != null) + { + send("sisi", true); + send("startpage", false); + } + } + + if (AppInit.conf.LampaWeb.initPlugins.sync) + send("sync", true); + + if (AppInit.conf.LampaWeb.initPlugins.timecode) + send("timecode", true); + + if (AppInit.conf.LampaWeb.initPlugins.bookmark) + send("bookmark", true); + + if (AppInit.conf.LampaWeb.initPlugins.torrserver && AppInit.modules.FirstOrDefault(i => i.dll == "TorrServer.dll" && i.enable) != null) + send("ts", true); + + if (AppInit.conf.LampaWeb.initPlugins.backup) + send("backup", true); + } + + if (plugins.Count == 0) + sb = sb.Replace("{plugins}", string.Empty); + else + { + sb = sb.Replace("{plugins}", string.Join(",", plugins)); + } + + sb = sb.Replace("{country}", requestInfo.Country) + .Replace("{localhost}", host); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + #region privateinit.js + [HttpGet] + [Route("privateinit.js")] + public ActionResult PrivateInit() + { + var user = requestInfo.user; + if (user == null || user.ban || DateTime.UtcNow > user.expires) + return Content(string.Empty, "application/javascript; charset=utf-8"); + + var sb = new StringBuilder(FileCache.ReadAllText("plugins/privateinit.js")); + + sb = sb.Replace("{country}", requestInfo.Country) + .Replace("{localhost}", host); + + if (AppInit.modules != null && AppInit.modules.FirstOrDefault(i => i.dll == "JacRed.dll" && i.enable) != null) + sb = sb.Replace("{jachost}", Regex.Replace(host, "^https?://", "")); + else + sb = sb.Replace("{jachost}", "redapi.apn.monster"); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/MediaController.cs b/BaseModule/Controllers/MediaController.cs new file mode 100644 index 0000000..2047a9f --- /dev/null +++ b/BaseModule/Controllers/MediaController.cs @@ -0,0 +1,203 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using Shared.Engine.Utilities; +using Shared.Models; +using Shared.Models.Base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Lampac.Controllers +{ + public class MediaController : BaseController + { + #region Routes + [HttpGet] + [AllowAnonymous] + [Route("/media/rsize/{token}/{width}/{height}/{*url}")] + public ActionResult Get(string token, int width, int height, string url) + { + return GetLocation(url + HttpContext.Request.QueryString.Value, new MediaRequestBase + { + type = "img", + auth_token = token, + width = width, + height = height + }, null, null); + } + + [HttpGet] + [AllowAnonymous] + [Route("/media/{type}/{token}/{*url}")] + public ActionResult Get(string type, string token, string url) + { + return GetLocation(url + HttpContext.Request.QueryString.Value, new MediaRequestBase + { + auth_token = token, + type = type + }, null, null); + } + + [HttpGet] + [AllowAnonymous] + [Route("/media")] + public ActionResult Get(string url, string headers, [FromQuery] MediaRequestBase request) + { + var webProxy = CreateProxy(request?.proxy, request?.proxy_name); + var headerList = HeadersModel.Init(ParseHeaders(headers)); + + return GetLocation(url, request, headerList, webProxy); + } + + [HttpPost] + [AllowAnonymous] + [Route("/media")] + public ActionResult Post([FromBody] MediaRequest request) + { + if (!TryValidateBase(request, out ActionResult errorResult)) + return errorResult; + + if (request.urls == null || request.urls.Count == 0) + return JsonError("invalid urls", 400); + + var webProxy = CreateProxy(request.proxy, request.proxy_name); + var headerList = HeadersModel.Init(request.headers); + var streamSettings = CreateStreamSettings(request); + + var result = new List(request.urls.Count); + + foreach (string source in request.urls) + { + string proxied = request.type == "img" + ? CreateImageProxy(source, request.width, request.height, headerList, webProxy) + : HostStreamProxy(streamSettings, source, headerList, webProxy); + + result.Add(proxied); + } + + return Json(new + { + success = true, + urls = result + }); + } + #endregion + + #region Helpers + ActionResult GetLocation(string url, MediaRequestBase request, List headers, WebProxy proxy) + { + if (string.IsNullOrEmpty(url)) + return JsonError("invalid url", 400); + + if (!TryValidateBase(request, out ActionResult errorResult)) + return errorResult; + + string location = request.type == "img" + ? CreateImageProxy(url, request.width, request.height, headers, proxy) + : HostStreamProxy(CreateStreamSettings(request), url, headers, proxy); + + return Redirect(location); + } + + BaseSettings CreateStreamSettings(MediaRequestBase request) + { + return new BaseSettings + { + plugin = "media", + streamproxy = true, + apnstream = request.apnstream, + useproxystream = request.useproxystream + }; + } + + bool TryValidateBase(MediaRequestBase request, out ActionResult errorResult) + { + errorResult = null; + var init = AppInit.conf.media; + + if (request == null) + { + errorResult = JsonError("invalid request", 400); + return false; + } + + if (string.IsNullOrEmpty(request.auth_token) || init?.tokens == null || !init.tokens.Any(t => t == request.auth_token)) + { + errorResult = JsonError("unauthorized", 401); + return false; + } + + return true; + } + + Dictionary ParseHeaders(string headers) + { + try + { + if (!string.IsNullOrEmpty(headers)) + return JsonConvert.DeserializeObject>(headers); + } + catch { } + + return null; + } + + WebProxy CreateProxy(string proxyValue, string proxyName) + { + ProxySettings proxySettings = null; + + if (!string.IsNullOrEmpty(proxyValue)) + { + proxySettings = new ProxySettings + { + list = [proxyValue] + }; + } + else if (!string.IsNullOrEmpty(proxyName) && AppInit.conf.globalproxy != null) + { + var settings = AppInit.conf.globalproxy.FirstOrDefault(i => i.name == proxyName); + if (settings?.list != null && settings.list.Length > 0) + proxySettings = settings; + } + + if (proxySettings == null) + return null; + + return ProxyManager.ConfigureWebProxy(proxySettings, proxySettings.list.First()).proxy; + } + + string CreateImageProxy(string url, int? width, int? height, List headers, WebProxy proxy) + { + if (!AppInit.conf.serverproxy.enable) + return url; + + string encrypted = ProxyLink.Encrypt(url, requestInfo.IP, headers, proxy, "posterapi", verifyip: false, IsProxyImg: true); + + if (AppInit.conf.accsdb.enable && !AppInit.conf.serverproxy.encrypt) + encrypted = AccsDbInvk.Args(encrypted, HttpContext); + + int normalizedWidth = Math.Max(0, width ?? 0); + int normalizedHeight = Math.Max(0, height ?? 0); + + if (normalizedWidth > 0 || normalizedHeight > 0) + return $"{host}/proxyimg:{normalizedWidth}:{normalizedHeight}/{encrypted}"; + + return $"{host}/proxyimg/{encrypted}"; + } + + ActionResult JsonError(string message, int statusCode) + { + HttpContext.Response.StatusCode = statusCode; + return ContentTo(JsonConvertPool.SerializeObject(new + { + success = false, + error = message + })); + } + #endregion + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/PlayerInnerController.cs b/BaseModule/Controllers/PlayerInnerController.cs new file mode 100644 index 0000000..8e7aa42 --- /dev/null +++ b/BaseModule/Controllers/PlayerInnerController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shared; +using System; +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace Lampac.Controllers +{ + public class PlayerInnerController : BaseController + { + [HttpGet] + [Route("player-inner/{*uri}")] + public void PlayerInner(string uri) + { + if (string.IsNullOrEmpty(AppInit.conf.playerInner)) + return; + + // убираем мусор в ссылке + uri = Regex.Replace(uri, "[^a-z0-9_:\\-\\/\\.\\=\\?\\&\\%\\@]+", "", RegexOptions.IgnoreCase); + uri = uri + HttpContext.Request.QueryString.Value; + + if (!Uri.TryCreate(uri, UriKind.Absolute, out var stream) || + (stream.Scheme != Uri.UriSchemeHttp && stream.Scheme != Uri.UriSchemeHttps)) + return; + + var _info = new ProcessStartInfo() + { + FileName = AppInit.conf.playerInner + }; + + _info.ArgumentList.Add(stream.AbsoluteUri); + + Process.Start(_info); + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/RchApiController.cs b/BaseModule/Controllers/RchApiController.cs new file mode 100644 index 0000000..bda27d2 --- /dev/null +++ b/BaseModule/Controllers/RchApiController.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Shared; +using Shared.Engine; +using System.IO.Compression; +using System.Threading.Tasks; + +namespace Lampac.Controllers +{ + public class RchBaseApi : BaseController + { + [HttpGet] + [Route("rch/check/connected")] + public ActionResult СheckСonnected() + { + var rch = new RchClient(HttpContext, host, new Shared.Models.Base.BaseSettings() { rhub = true }, requestInfo); + if (rch.IsNotConnected()) + return ContentTo(rch.connectionMsg); + + var info = rch.InfoConnected() ?? new RchClientInfo(); + return Json(new { info.version, info.apkVersion, info.rchtype }); + } + } + + public class RchApi : Controller + { + [HttpPost] + [AllowAnonymous] + [Route("rch/result")] + async public Task WriteResult([FromQuery] string id) + { + if (string.IsNullOrEmpty(id)) + return BadRequest(401); + + if (!RchClient.rchIds.TryGetValue(id, out var rchHub)) + return BadRequest(400); + + try + { + await Request.Body.CopyToAsync(rchHub.ms, PoolInvk.bufferSize, HttpContext.RequestAborted); + rchHub.ms.Position = 0; + + rchHub.tcs.TrySetResult(null); + } + catch + { + rchHub.tcs.TrySetResult(null); + return BadRequest(400); + } + + return Ok(); + } + + [HttpPost] + [AllowAnonymous] + [Route("rch/gzresult")] + async public Task WriteZipResult([FromQuery] string id) + { + if (string.IsNullOrEmpty(id)) + return BadRequest(401); + + if (!RchClient.rchIds.TryGetValue(id, out var rchHub)) + return BadRequest(400); + + try + { + using (var gzip = new GZipStream(Request.Body, CompressionMode.Decompress, leaveOpen: true)) + { + await gzip.CopyToAsync(rchHub.ms, PoolInvk.bufferSize, HttpContext.RequestAborted); + rchHub.ms.Position = 0; + + rchHub.tcs.TrySetResult(null); + } + } + catch + { + rchHub.tcs.TrySetResult(null); + return BadRequest(400); + } + + return Ok(); + } + } +} diff --git a/BaseModule/Controllers/StorageController.cs b/BaseModule/Controllers/StorageController.cs new file mode 100644 index 0000000..e9530e2 --- /dev/null +++ b/BaseModule/Controllers/StorageController.cs @@ -0,0 +1,356 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Engine.Utilities; +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using IO = System.IO; + +namespace Lampac.Controllers +{ + public class StorageController : BaseController + { + #region StorageController + static StorageController() + { + Directory.CreateDirectory("database/storage"); + Directory.CreateDirectory("database/storage/temp"); + } + #endregion + + #region backup.js + [HttpGet] + [AllowAnonymous] + [Route("backup.js")] + [Route("backup/js/{token}")] + public ActionResult Backup(string token) + { + if (!AppInit.conf.storage.enable) + return Content(string.Empty, "application/javascript; charset=utf-8"); + + var sb = new StringBuilder(FileCache.ReadAllText("plugins/backup.js")); + + sb.Replace("{localhost}", host) + .Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + #region sync.js + [HttpGet] + [AllowAnonymous] + [Route("sync.js")] + [Route("sync/js/{token}")] + public ActionResult SyncJS(string token, bool lite) + { + if (!AppInit.conf.storage.enable) + return Content(string.Empty, "application/javascript; charset=utf-8"); + + StringBuilder sb; + + if (lite || AppInit.conf.sync_user.version == 1) + { + sb = new StringBuilder(FileCache.ReadAllText($"plugins/{(lite ? "sync_lite" : "sync")}.js")); + } + else + { + sb = new StringBuilder(FileCache.ReadAllText("plugins/sync_v2/sync.js")); + } + + sb.Replace("{sync-invc}", FileCache.ReadAllText("plugins/sync-invc.js")) + .Replace("{localhost}", host) + .Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + + #region Get + [HttpGet] + [Route("/storage/get")] + async public Task Get(string path, string pathfile, bool responseInfo) + { + if (!AppInit.conf.storage.enable) + return ContentTo("{\"success\": false, \"msg\": \"disabled\"}"); + + string outFile = getFilePath(path, pathfile, false); + if (outFile == null || !IO.File.Exists(outFile)) + return ContentTo("{\"success\": false, \"msg\": \"outFile\"}"); + + var file = new FileInfo(outFile); + var fileInfo = new { file.Name, path = outFile, file.Length, changeTime = new DateTimeOffset(file.LastWriteTimeUtc).ToUnixTimeMilliseconds() }; + + if (responseInfo) + return Json(new { success = true, uid = requestInfo.user_uid, fileInfo }); + + string data; + + if (AppInit.conf.storage.brotli) + { + data = await BrotliTo.DecompressAsync(outFile); + } + else + { + var semaphore = new SemaphorManager(outFile, TimeSpan.FromSeconds(20)); + + try + { + await semaphore.WaitAsync(); + + data = await IO.File.ReadAllTextAsync(outFile); + } + catch + { + HttpContext.Response.StatusCode = 503; + return ContentTo("{\"success\": false, \"msg\": \"fileLock\"}"); + } + finally + { + semaphore.Release(); + } + } + + return Json(new { success = true, uid = requestInfo.user_uid, fileInfo, data }); + } + #endregion + + #region Set + [HttpPost] + [Route("/storage/set")] + async public Task Set([FromQuery]string path, [FromQuery]string pathfile, [FromQuery]string connectionId, [FromQuery]string events) + { + if (!AppInit.conf.storage.enable) + return ContentTo("{\"success\": false, \"msg\": \"disabled\"}"); + + if (HttpContext.Request.ContentLength > AppInit.conf.storage.max_size) + return ContentTo("{\"success\": false, \"msg\": \"max_size\"}"); + + string outFile = getFilePath(path, pathfile, true); + if (outFile == null) + return ContentTo("{\"success\": false, \"msg\": \"outFile\"}"); + + using (var memoryStream = PoolInvk.msm.GetStream()) + { + try + { + await HttpContext.Request.Body.CopyToAsync(memoryStream); + memoryStream.Position = 0; + } + catch + { + HttpContext.Response.StatusCode = 503; + return ContentTo("{\"success\": false, \"msg\": \"Request.Body.CopyToAsync\"}"); + } + + if (AppInit.conf.storage.brotli) + { + await BrotliTo.CompressAsync(outFile, memoryStream); + } + else + { + var semaphore = new SemaphorManager(outFile, TimeSpan.FromSeconds(20)); + + try + { + await semaphore.WaitAsync(); + + using (var fileStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + await memoryStream.CopyToAsync(fileStream, PoolInvk.bufferSize); + } + catch + { + HttpContext.Response.StatusCode = 503; + return ContentTo("{\"success\": false, \"msg\": \"fileLock\"}"); + } + finally + { + semaphore.Release(); + } + } + } + + #region events + if (!string.IsNullOrEmpty(events)) + { + try + { + var json = JsonConvert.DeserializeObject(CrypTo.DecodeBase64(events)); + _ = Shared.Startup.WS.EventsAsync(json.Value("connectionId"), requestInfo.user_uid, json.Value("name"), json.Value("data")).ConfigureAwait(false); + _ = Shared.Startup.Nws.EventsAsync(json.Value("connectionId"), requestInfo.user_uid, json.Value("name"), json.Value("data")).ConfigureAwait(false); + } + catch { } + } + else + { + string edata = JsonConvertPool.SerializeObject(new { path, pathfile }); + _ = Shared.Startup.Nws.EventsAsync(connectionId, requestInfo.user_uid, "storage", edata).ConfigureAwait(false); + } + #endregion + + var inf = new FileInfo(outFile); + + return Json(new + { + success = true, + uid = requestInfo.user_uid, + fileInfo = new { inf.Name, path = outFile, inf.Length, changeTime = new DateTimeOffset(inf.LastWriteTimeUtc).ToUnixTimeMilliseconds() } + }); + } + #endregion + + #region TempGet + [HttpGet] + [Route("/storage/temp/{key}")] + async public Task TempGet(string key, bool responseInfo) + { + if (!AppInit.conf.storage.enable) + return ContentTo("{\"success\": false, \"msg\": \"disabled\"}"); + + string outFile = getFilePath("temp", null, false, user_uid: key); + if (outFile == null || !IO.File.Exists(outFile)) + return ContentTo("{\"success\": false, \"msg\": \"outFile\"}"); + + var file = new FileInfo(outFile); + var fileInfo = new { file.Name, path = outFile, file.Length, changeTime = new DateTimeOffset(file.LastWriteTimeUtc).ToUnixTimeMilliseconds() }; + + if (responseInfo) + return Json(new { success = true, uid = requestInfo.user_uid, fileInfo }); + + string data; + + if (AppInit.conf.storage.brotli) + { + data = await BrotliTo.DecompressAsync(outFile); + } + else + { + var semaphore = new SemaphorManager(outFile, TimeSpan.FromSeconds(20)); + + try + { + await semaphore.WaitAsync(); + + data = await IO.File.ReadAllTextAsync(outFile); + } + catch + { + HttpContext.Response.StatusCode = 503; + return ContentTo("{\"success\": false, \"msg\": \"fileLock\"}"); + } + finally + { + semaphore.Release(); + } + } + + return Json(new { success = true, uid = requestInfo.user_uid, fileInfo, data }); + } + #endregion + + #region TempSet + [HttpPost] + [Route("/storage/temp/{key}")] + async public Task TempSet(string key) + { + if (!AppInit.conf.storage.enable) + return ContentTo("{\"success\": false, \"msg\": \"disabled\"}"); + + if (HttpContext.Request.ContentLength > AppInit.conf.storage.max_size) + return ContentTo("{\"success\": false, \"msg\": \"max_size\"}"); + + string outFile = getFilePath("temp", null, true, user_uid: key); + if (outFile == null) + return ContentTo("{\"success\": false, \"msg\": \"outFile\"}"); + + using (var memoryStream = PoolInvk.msm.GetStream()) + { + try + { + await HttpContext.Request.Body.CopyToAsync(memoryStream); + memoryStream.Position = 0; + } + catch + { + HttpContext.Response.StatusCode = 503; + return ContentTo("{\"success\": false, \"msg\": \"Request.Body.CopyToAsync\"}"); + } + + if (AppInit.conf.storage.brotli) + { + await BrotliTo.CompressAsync(outFile, memoryStream); + } + else + { + var semaphore = new SemaphorManager(outFile, TimeSpan.FromSeconds(20)); + + try + { + await semaphore.WaitAsync(); + + using (var fileStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + await memoryStream.CopyToAsync(fileStream, PoolInvk.bufferSize); + } + catch + { + HttpContext.Response.StatusCode = 503; + return ContentTo("{\"success\": false, \"msg\": \"fileLock\"}"); + } + finally + { + semaphore.Release(); + } + } + } + + var inf = new FileInfo(outFile); + + return Json(new + { + success = true, + uid = requestInfo.user_uid, + fileInfo = new { inf.Name, path = outFile, inf.Length, changeTime = new DateTimeOffset(inf.LastWriteTimeUtc).ToUnixTimeMilliseconds() } + }); + } + #endregion + + + #region getFilePath + string getFilePath(string path, string pathfile, bool createDirectory, string user_uid = null) + { + if (path == "temp" && string.IsNullOrEmpty(user_uid)) + return null; + + path = Regex.Replace(path, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase); + + string id = user_uid ?? requestInfo.user_uid; + if (string.IsNullOrEmpty(id)) + return null; + + id += pathfile; + string md5key = AppInit.conf.storage.md5name ? CrypTo.md5(id) : Regex.Replace(id, "[^a-z0-9\\-]", ""); + + if (path == "temp") + { + return $"database/storage/{path}/{md5key}"; + } + else + { + if (createDirectory) + Directory.CreateDirectory($"database/storage/{path}/{md5key.Substring(0, 2)}"); + + return $"database/storage/{path}/{md5key.Substring(0, 2)}/{md5key.Substring(2)}"; + } + } + #endregion + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/SyncApiController.cs b/BaseModule/Controllers/SyncApiController.cs new file mode 100644 index 0000000..48c8814 --- /dev/null +++ b/BaseModule/Controllers/SyncApiController.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Shared; +using IO = System.IO; + +namespace Lampac.Controllers +{ + public class SyncApiController : BaseController + { + [HttpGet] + [Route("/api/sync")] + public ActionResult Sync() + { + var sync = AppInit.conf.sync; + if (!requestInfo.IsLocalRequest || !sync.enable || sync.type != "master") + return Content("error"); + + if (sync.initconf == "current") + return Content(JsonConvert.SerializeObject(AppInit.conf), "application/json; charset=utf-8"); + + var init = new AppInit(); + + string confile = "sync.conf"; + if (sync.override_conf != null && sync.override_conf.TryGetValue(requestInfo.IP, out string _conf)) + confile = _conf; + + if (IO.File.Exists(confile)) + init = JsonConvert.DeserializeObject(IO.File.ReadAllText(confile)); + + init.accsdb.users = AppInit.conf.accsdb.users; + + string json = JsonConvert.SerializeObject(init); + json = json.Replace("{server_ip}", requestInfo.IP); + + return Content(json, "application/json; charset=utf-8"); + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/TimecodeController.cs b/BaseModule/Controllers/TimecodeController.cs new file mode 100644 index 0000000..abd73ae --- /dev/null +++ b/BaseModule/Controllers/TimecodeController.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.SQL; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web; + +namespace Lampac.Controllers +{ + public class TimecodeController : BaseController + { + #region timecode.js + [HttpGet] + [AllowAnonymous] + [Route("timecode.js")] + [Route("timecode/js/{token}")] + public ActionResult timecode(string token) + { + string file = FileCache.ReadAllText("plugins/timecode.js").Replace("{localhost}", host); + file = file.Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(file, contentType: "application/javascript; charset=utf-8"); + } + #endregion + + [HttpGet] + [Route("/timecode/all")] + async public Task Get(string card_id) + { + if (string.IsNullOrEmpty(card_id)) + return Json(new { }); + + string userId = getUserid(requestInfo, HttpContext); + + Dictionary timecodes = null; + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + timecodes = await sqlDb.timecodes + .AsNoTracking() + .Where(i => i.user == userId && i.card == card_id) + .ToDictionaryAsync(i => i.item, i => i.data); + } + + if (timecodes == null || timecodes.Count == 0) + return Json(new { }); + + return Json(timecodes); + } + + [HttpPost] + [Route("/timecode/add")] + async public Task Set([FromQuery] string card_id, [FromForm] string id, [FromForm] string data) + { + if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(data)) + return ContentTo("{\"success\": false}"); + + if (string.IsNullOrEmpty(card_id)) + return ContentTo("{\"success\": false}"); + + string userId = getUserid(requestInfo, HttpContext); + + bool success = false; + + try + { + await SyncUserContext.semaphore.WaitAsync(TimeSpan.FromSeconds(30)); + + using (var sqlDb = SyncUserContext.Factory != null + ? SyncUserContext.Factory.CreateDbContext() + : new SyncUserContext()) + { + sqlDb.timecodes + .Where(i => i.user == userId && i.card == card_id && i.item == id) + .ExecuteDelete(); + + sqlDb.timecodes.Add(new SyncUserTimecodeSqlModel + { + user = userId, + card = card_id, + item = id, + data = data, + updated = DateTime.UtcNow + }); + + success = await sqlDb.SaveChangesAsync() > 0; + } + } + catch { } + finally + { + SyncUserContext.semaphore.Release(); + } + + return ContentTo($"{{\"success\": {success.ToString().ToLower()}}}"); + } + + + static string getUserid(RequestModel requestInfo, HttpContext httpContext) + { + string user_id = requestInfo.user_uid; + + if (httpContext.Request.Query.TryGetValue("profile_id", out var profile_id) && !string.IsNullOrEmpty(profile_id) && profile_id != "0") + return $"{user_id}_{profile_id}"; + + return user_id; + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/TmdbController.cs b/BaseModule/Controllers/TmdbController.cs new file mode 100644 index 0000000..32a08fa --- /dev/null +++ b/BaseModule/Controllers/TmdbController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shared; +using Shared.Engine; +using System.Web; + +namespace Lampac.Controllers +{ + public class TmdbController : BaseController + { + [HttpGet] + [AllowAnonymous] + [Route("tmdbproxy.js")] + [Route("tmdbproxy/js/{token}")] + public ActionResult TmdbProxy(string token) + { + string file = FileCache.ReadAllText("plugins/tmdbproxy.js").Replace("{localhost}", host); + file = file.Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(file, contentType: "application/javascript; charset=utf-8"); + } + } +} \ No newline at end of file diff --git a/BaseModule/Controllers/WebLogController.cs b/BaseModule/Controllers/WebLogController.cs new file mode 100644 index 0000000..05324a2 --- /dev/null +++ b/BaseModule/Controllers/WebLogController.cs @@ -0,0 +1,204 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Shared; + +namespace Lampac.Controllers +{ + public class WebLogController : BaseController + { + [HttpGet] + [AllowAnonymous] + [Route("weblog")] + public ActionResult WebLog(string token, string pattern, string receive = "http") + { + if (!AppInit.conf.weblog.enable) + return Content("Включите weblog в init.conf\n\n\"weblog\": {\n \"enable\": true\n}", contentType: "text/plain; charset=utf-8"); + + if (!string.IsNullOrEmpty(AppInit.conf.weblog.token) && token != AppInit.conf.weblog.token) + return Content("Используйте /weblog?token=my_key\n\n\"weblog\": {\n \"enable\": true,\n \"token\": \"my_key\"\n}", contentType: "text/plain; charset=utf-8"); + + string html = $@" + + + + weblog + + + +
+ + + +
+
+ + + + +"; + + return Content(html, "text/html; charset=utf-8"); + } + + + static string nwsCode(string token) => $@" +const client = new NativeWsClient(""/nws"", {{ + autoReconnect: true, + reconnectDelay: 2000, + + onOpen: function () {{ + send('WebSocket connected'); + outageReported = false; + client.invoke('RegistryWebLog', '{token}'); + }}, + + onClose: function () {{ + reportOutageOnce('Connection closed'); + }}, + + onError: function (err) {{ + reportOutageOnce('Connection error: ' + (err && err.message ? err.message : String(err))); + }} +}}); + +client.on('Receive', function (message, e) {{ + if (receive === e) send(message); +}}); + +client.connect(); +"; + + static string signalCode(string token) => $@" +const hubConnection = new signalR.HubConnectionBuilder() + .withUrl('/ws') + .build(); + +let reconnectAttempts = 0; +const maxReconnectAttempts = 150; // 5 minutes +const reconnectDelay = 2000; // 2 seconds + +function startConnection() {{ + hubConnection.start() + .then(function () {{ + if (reconnectAttempts != 0) + send('WebSocket connected'); + reconnectAttempts = 0; // Reset counter on successful connection + hubConnection.invoke('RegistryWebLog', '{token}'); + }}) + .catch(function (err) {{ + console.log(`${{err.toString()}}\n\nAttempting to reconnect (${{reconnectAttempts}}/${{maxReconnectAttempts}})...`); + attemptReconnect(); + }}); +}} + +function attemptReconnect() {{ + if (reconnectAttempts < maxReconnectAttempts) {{ + reconnectAttempts++; + setTimeout(function() {{ + startConnection(); + }}, reconnectDelay); + }} else {{ + send('Max reconnection attempts reached. Please refresh the page.'); + }} +}} + +hubConnection.on('Receive', function(message, e) {{ + if(receive === e) send(message); +}}); + +hubConnection.onclose(function(err) {{ + if (err) {{ + send('Connection closed due to error: ' + err.toString()); + }} else {{ + send('Connection closed'); + }} + attemptReconnect(); +}}); + +startConnection(); +"; + } +} \ No newline at end of file diff --git a/Build/Docker/amd64 b/Build/Docker/amd64 new file mode 100644 index 0000000..800c223 --- /dev/null +++ b/Build/Docker/amd64 @@ -0,0 +1,34 @@ +FROM debian:12.5-slim + +EXPOSE 9118 +WORKDIR /home + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip sed chromium xvfb libnspr4 fontconfig \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -fSL -k -o dotnet.tar.gz https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.12/aspnetcore-runtime-9.0.12-linux-x64.tar.gz \ + && mkdir -p /usr/share/dotnet \ + && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet \ + && rm dotnet.tar.gz + +RUN curl -L -k -o publish.zip https://github.com/immisterio/Lampac/releases/latest/download/publish.zip \ + && unzip -o publish.zip && rm -f publish.zip && rm -rf merchant \ + && rm -rf runtimes/os* && rm -rf runtimes/win* && rm -rf runtimes/linux-arm runtimes/linux-arm64 runtimes/linux-musl-arm64 runtimes/linux-musl-x64 \ + && touch isdocker + +RUN curl -k -s https://raw.githubusercontent.com/immisterio/Lampac/main/Build/Docker/update.sh | bash + +RUN mkdir -p torrserver && curl -L -k -o torrserver/TorrServer-linux https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-amd64 \ + && chmod +x torrserver/TorrServer-linux + +RUN mkdir -p .playwright/node/linux-x64 && curl -L -k -o .playwright/node/linux-x64/node https://github.com/immisterio/playwright/releases/download/chrome/node-linux-x64 \ + && chmod +x .playwright/node/linux-x64/node && touch .playwright/node/linux-x64/node.ok + +RUN curl -L -k -o ffmpeg.zip https://github.com/immisterio/ffmpeg/releases/download/ffmpeg2/ffmpeg-master-latest-linux64-gpl.zip \ + && unzip -o ffmpeg.zip && rm -f ffmpeg.zip \ + && mv ffprobe data/ffprobe && chmod +x data/ffprobe \ + && mv ffmpeg data/ffmpeg && chmod +x data/ffmpeg + +RUN echo '{"chromium":{"executablePath":"/usr/bin/chromium"}}' > init.conf + +ENTRYPOINT ["/usr/share/dotnet/dotnet", "Lampac.dll"] diff --git a/Build/Docker/arm32 b/Build/Docker/arm32 new file mode 100644 index 0000000..e1d85ff --- /dev/null +++ b/Build/Docker/arm32 @@ -0,0 +1,35 @@ +FROM arm32v7/debian:12.5-slim + +EXPOSE 9118 +WORKDIR /home + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip sed chromium xvfb libnspr4 \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -L -k -o ffprobe.zip https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffprobe-6.1-linux-armhf-32.zip \ + && unzip -o ffprobe.zip && rm -f ffprobe.zip \ + && mv ffprobe /usr/bin/ffprobe && chmod +x /usr/bin/ffprobe + +RUN curl -fSL -k -o dotnet.tar.gz https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.12/aspnetcore-runtime-9.0.12-linux-arm.tar.gz \ + && mkdir -p /usr/share/dotnet \ + && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet \ + && rm dotnet.tar.gz + +RUN curl -L -k -o publish.zip https://github.com/immisterio/Lampac/releases/latest/download/publish.zip \ + && unzip -o publish.zip && rm -f publish.zip && rm -rf merchant \ + && rm -rf runtimes/os* && rm -rf runtimes/win* && rm -rf runtimes/linux-arm64 runtimes/linux-musl-arm64 runtimes/linux-musl-x64 runtimes/linux-x64 \ + && touch isdocker + +RUN curl -k -s https://raw.githubusercontent.com/immisterio/Lampac/main/Build/Docker/update.sh | bash + +RUN mkdir -p torrserver && curl -L -k -o torrserver/TorrServer-linux https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-arm7 \ + && chmod +x torrserver/TorrServer-linux + +RUN mkdir -p .playwright/node/linux-arm && curl -L -k -o .playwright/node/linux-arm/node https://github.com/immisterio/playwright/releases/download/chrome/node-linux-armv7l \ + && chmod +x .playwright/node/linux-arm/node && touch .playwright/node/linux-arm/node.ok + +RUN echo '{"chromium":{"executablePath":"/usr/bin/chromium"},"typecache":"mem","isarm":true,"mikrotik":true,"GC":{"enable":true,"Concurrent":false,"ConserveMemory":9,"HighMemoryPercent":1,"RetainVM":false},"WAF":{"enable":false,"bypassLocalIP":true,"allowExternalIpAccess":true,"bruteForceProtection":false},"serverproxy":{"verifyip":false,"image":{"cache": false,"cache_rsize":false}}}' > init.conf + +RUN echo '{"runtimeOptions":{"tfm":"net9.0","frameworks":[{"name":"Microsoft.NETCore.App","version":"9.0.0"},{"name":"Microsoft.AspNetCore.App","version":"9.0.0"}],"configProperties":{"System.GC.Server":false,"System.Reflection.Metadata.MetadataUpdater.IsSupported":false,"System.Reflection.NullabilityInfoContext.IsSupported":true,"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization":false}}}' > Lampac.runtimeconfig.json + +ENTRYPOINT ["/usr/share/dotnet/dotnet", "Lampac.dll"] diff --git a/Build/Docker/arm64 b/Build/Docker/arm64 new file mode 100644 index 0000000..d51e175 --- /dev/null +++ b/Build/Docker/arm64 @@ -0,0 +1,36 @@ +FROM arm64v8/debian:12.5-slim + +EXPOSE 9118 +WORKDIR /home + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip sed chromium xvfb libnspr4 fontconfig \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -fSL -k -o dotnet.tar.gz https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.12/aspnetcore-runtime-9.0.12-linux-arm64.tar.gz \ + && mkdir -p /usr/share/dotnet \ + && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet \ + && rm dotnet.tar.gz + +RUN curl -L -k -o publish.zip https://github.com/immisterio/Lampac/releases/latest/download/publish.zip \ + && unzip -o publish.zip && rm -f publish.zip && rm -rf merchant \ + && rm -rf runtimes/os* && rm -rf runtimes/win* && rm -rf runtimes/linux-arm runtimes/linux-musl-arm64 runtimes/linux-musl-x64 runtimes/linux-x64 \ + && touch isdocker + +RUN curl -k -s https://raw.githubusercontent.com/immisterio/Lampac/main/Build/Docker/update.sh | bash + +RUN mkdir -p torrserver && curl -L -k -o torrserver/TorrServer-linux https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-arm64 \ + && chmod +x torrserver/TorrServer-linux + +RUN mkdir -p .playwright/node/linux-arm64 && curl -L -k -o .playwright/node/linux-arm64/node https://github.com/immisterio/playwright/releases/download/chrome/node-linux-arm64 \ + && chmod +x .playwright/node/linux-arm64/node && touch .playwright/node/linux-arm64/node.ok + +RUN curl -L -k -o ffmpeg.zip https://github.com/immisterio/ffmpeg/releases/download/ffmpeg2/ffmpeg-master-latest-linuxarm64-gpl.zip \ + && unzip -o ffmpeg.zip && rm -f ffmpeg.zip \ + && mv ffprobe data/ffprobe && chmod +x data/ffprobe \ + && mv ffmpeg data/ffmpeg && chmod +x data/ffmpeg + +RUN echo '{"chromium":{"executablePath":"/usr/bin/chromium"},"typecache":"mem","isarm":true,"mikrotik":true,"GC":{"enable":true,"Concurrent":false,"ConserveMemory":9,"HighMemoryPercent":1,"RetainVM":false},"WAF":{"enable":false,"bypassLocalIP":true,"allowExternalIpAccess":true,"bruteForceProtection":false},"serverproxy":{"verifyip":false,"image":{"cache": false,"cache_rsize":false}}}' > init.conf + +RUN echo '{"runtimeOptions":{"tfm":"net9.0","frameworks":[{"name":"Microsoft.NETCore.App","version":"9.0.0"},{"name":"Microsoft.AspNetCore.App","version":"9.0.0"}],"configProperties":{"System.GC.Server":false,"System.Reflection.Metadata.MetadataUpdater.IsSupported":false,"System.Reflection.NullabilityInfoContext.IsSupported":true,"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization":false}}}' > Lampac.runtimeconfig.json + +ENTRYPOINT ["/usr/share/dotnet/dotnet", "Lampac.dll"] \ No newline at end of file diff --git a/Build/Docker/koyeb b/Build/Docker/koyeb new file mode 100644 index 0000000..59b117a --- /dev/null +++ b/Build/Docker/koyeb @@ -0,0 +1,30 @@ +FROM debian:12.5-slim + +EXPOSE 8000 +WORKDIR /home + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip libicu-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -fSL -k -o dotnet.tar.gz https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.12/aspnetcore-runtime-9.0.12-linux-x64.tar.gz \ + && mkdir -p /usr/share/dotnet \ + && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet \ + && rm dotnet.tar.gz + +RUN curl -L -k -o publish.zip https://github.com/immisterio/Lampac/releases/latest/download/publish.zip \ + && unzip -o publish.zip && rm -f publish.zip && rm -rf merchant \ + && rm -rf runtimes/os* && rm -rf runtimes/win* && rm -rf runtimes/linux-arm runtimes/linux-arm64 runtimes/linux-musl-arm64 runtimes/linux-musl-x64 \ + && touch isdocker + +RUN curl -k -s https://raw.githubusercontent.com/immisterio/Lampac/main/Build/Docker/update.sh | bash + +RUN echo '{"listen":{"port":8000,"scheme":"https","frontend":"cloudflare"},"KnownProxies":[{"ip":"0.0.0.0","prefixLength":0}],"mikrotik":true,"typecache":"mem","GC":{"enable":true,"Concurrent":false,"ConserveMemory":9,"HighMemoryPercent":1,"RetainVM":false},"WAF":{"enable":false,"bypassLocalIP":true,"allowExternalIpAccess":true,"bruteForceProtection":false},"watcherInit":"cron","pirate_store":false,"rch":{"keepalive":900},"weblog":{"enable":true},"chromium":{"enable":false},"firefox":{"enable":false},"LampaWeb":{"autoupdate":false,"initPlugins":{"timecode":false,"backup":false,"sync":false}},"cub":{"enable":true,"geo":["RU"]},"tmdb":{"enable":true},"serverproxy":{"verifyip":false,"buffering":{"enable":false},"image":{"cache":false,"cache_rsize":false}},"online":{"checkOnlineSearch":false},"sisi":{"push_all":false,"rsize_disable":["BongaCams","Chaturbate","Runetki","PornHub","Eporner","HQporner","Spankbang","Porntrex","Xnxx","Xvideos","Xhamster","Tizam"],"proxyimg_disable":["Ebalovo"]},"Mirage":{"displayindex":1},"Ashdi":{"rhub":true},"Kinoukr":{"rhub":true},"VDBmovies":{"rhub":true},"VideoDB":{"rhub":true},"FanCDN":{"rhub":true},"Rezka":{"rhub":true,"scheme":"https"},"Kinotochka":{"rhub":true,"rhub_streamproxy":true,"streamproxy":false,"geostreamproxy":null,"rhub_geo_disable":["RU"]},"Videoseed":{"streamproxy":false,"geostreamproxy":null},"Vibix":{"streamproxy":false,"geostreamproxy":null},"iRemux":{"streamproxy":false,"geostreamproxy":null},"Rgshows":{"streamproxy":false,"geostreamproxy":null},"Autoembed":{"enable":false},"Animevost":{"rhub":true},"AnilibriaOnline":{"rhub":true},"Ebalovo":{"rhub":true},"Spankbang":{"rhub":true,"rhub_geo_disable":["RU"]},"BongaCams":{"rhub":true},"Chaturbate":{"rhub":true,"rhub_geo_disable":["RU"]},"Runetki":{"rhub":true},"HQporner":{"rhub":true,"streamproxy":false,"geostreamproxy":null,"qualitys_proxy":false,"geo_hide":["RU"]},"Eporner":{"streamproxy":false,"geostreamproxy":null,"qualitys_proxy":false,"rhub_geo_disable":["RU"]},"Porntrex":{"rhub":true,"streamproxy":false,"geostreamproxy":null,"qualitys_proxy":false,"rhub_geo_disable":["RU"]},"Xhamster":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["RU"]},"Xnxx":{"rhub":true,"rhub_fallback":true,"rhub_streamproxy":true,"rhub_geo_disable":["RU"]},"Tizam":{"rhub":true,"rhub_fallback":true,"streamproxy":false,"geostreamproxy":null,"qualitys_proxy":false},"Xvideos":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["RU"]},"PornHub":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"RutubeMovie":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["UA"]},"VkMovie":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"Plvideo":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["UA"]},"CDNvideohub":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"Redheadsound":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"CDNmovies":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"AniMedia":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"Animebesst":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true}}' > /home/init.conf + +RUN echo '"typesearch":"webapi","merge":null' > /home/module/JacRed.conf + +RUN echo '[{"enable":true,"dll":"SISI.dll"},{"enable":true,"dll":"Online.dll"},{"enable":true,"initspace":"Catalog.ModInit","dll":"Catalog.dll"},{"enable":true,"initspace":"TorrServer.ModInit","dll":"TorrServer.dll"},{"enable":true,"initspace":"Jackett.ModInit","dll":"JacRed.dll"}]' > /home/module/manifest.json + +RUN mkdir -p torrserver && curl -L -k -o torrserver/TorrServer-linux https://github.com/YouROK/TorrServer/releases/latest/download/TorrServer-linux-amd64 \ + && chmod +x torrserver/TorrServer-linux + +ENTRYPOINT ["/usr/share/dotnet/dotnet", "Lampac.dll"] \ No newline at end of file diff --git a/Build/Docker/mircloud b/Build/Docker/mircloud new file mode 100644 index 0000000..7d71f05 --- /dev/null +++ b/Build/Docker/mircloud @@ -0,0 +1,25 @@ +FROM debian:12.5-slim + +EXPOSE 80 +WORKDIR /home + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl sed unzip \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -fSL -k -o dotnet.tar.gz https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.12/aspnetcore-runtime-9.0.12-linux-x64.tar.gz \ + && mkdir -p /usr/share/dotnet \ + && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet \ + && rm dotnet.tar.gz + +RUN curl -L -k -o publish.zip https://github.com/immisterio/Lampac/releases/latest/download/publish.zip \ + && unzip -o publish.zip && rm -f publish.zip && rm -rf merchant \ + && rm -rf runtimes/os* && rm -rf runtimes/win* && rm -rf runtimes/linux-arm runtimes/linux-arm64 runtimes/linux-musl-arm64 runtimes/linux-musl-x64 \ + && touch isdocker + +RUN curl -k -s https://raw.githubusercontent.com/immisterio/Lampac/main/Build/Docker/update.sh | bash + +RUN echo '{"listen":{"port":80,"scheme":"https"},"KnownProxies":[{"ip":"0.0.0.0","prefixLength":0}],"rch":{"enable":true},"typecache":"mem","GC":{"enable":true,"Concurrent":false,"ConserveMemory":9,"HighMemoryPercent":1,"RetainVM":false},"mikrotik":true,"serverproxy":{"verifyip":false,"showOrigUri":true,"buffering":{"enable":false}},"pirate_store": false,"dlna":{"enable":false},"chromium":{"enable":false},"online":{"checkOnlineSearch":true},"Rezka":{"host":"https://hdrezka.me","corseu":true,"xrealip":true,"uacdn":"https://prx-ams.ukrtelcdn.net","hls":false},"Zetflix":{"enable":false},"VDBmovies":{"enable":false},"iRemux":{"streamproxy":false,"geostreamproxy":["UA"]},"Kinobase":{"rhub":true},"Eneyida":{"rhub":true},"Kinoukr":{"rhub":true},"Kodik":{"enable":false},"AnimeGo":{"enable": false},"Animebesst":{"enable": false},"Eporner":{"streamproxy":false},"PornHub":{"enable":false},"Ebalovo":{"enable":false}}' > /home/init.conf + +RUN echo '[{"enable":true,"dll":"SISI.dll"},{"enable":true,"dll":"Online.dll"},{"enable":true,"initspace":"Jackett.ModInit","dll":"JacRed.dll"}]' > /home/module/manifest.json + +ENTRYPOINT ["/usr/share/dotnet/dotnet", "Lampac.dll"] diff --git a/Build/Docker/northflank b/Build/Docker/northflank new file mode 100644 index 0000000..cdfe0e5 --- /dev/null +++ b/Build/Docker/northflank @@ -0,0 +1,27 @@ +FROM debian:12.5-slim + +EXPOSE 80 +WORKDIR /home + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl unzip libicu-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -fSL -k -o dotnet.tar.gz https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.12/aspnetcore-runtime-9.0.12-linux-x64.tar.gz \ + && mkdir -p /usr/share/dotnet \ + && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet \ + && rm dotnet.tar.gz + +RUN curl -L -k -o publish.zip https://github.com/immisterio/Lampac/releases/latest/download/publish.zip \ + && unzip -o publish.zip && rm -f publish.zip && rm -rf merchant \ + && rm -rf runtimes/os* && rm -rf runtimes/win* && rm -rf runtimes/linux-arm runtimes/linux-arm64 runtimes/linux-musl-arm64 runtimes/linux-musl-x64 \ + && touch isdocker + +RUN curl -k -s https://raw.githubusercontent.com/immisterio/Lampac/main/Build/Docker/update.sh | bash + +RUN echo '{"listen":{"port":80,"scheme":"https"},"KnownProxies":[{"ip":"0.0.0.0","prefixLength":0}],"mikrotik":true,"typecache":"mem","GC":{"enable":true,"Concurrent":false,"ConserveMemory":9,"HighMemoryPercent":1,"RetainVM":false},"watcherInit":"cron","pirate_store":false,"rch":{"keepalive":900},"weblog":{"enable":true},"chromium":{"enable":false},"firefox":{"enable":false},"LampaWeb":{"autoupdate":false,"initPlugins":{"timecode":false,"backup":false,"sync":false}},"cub":{"enable":true,"geo":["RU"]},"tmdb":{"enable":true},"serverproxy":{"verifyip":false,"buffering":{"enable":false},"image":{"cache":false,"cache_rsize":false}},"online":{"checkOnlineSearch":false},"sisi":{"push_all":false,"rsize_disable":["BongaCams","Chaturbate","Runetki","PornHub","Eporner","HQporner","Spankbang","Porntrex","Xnxx","Xvideos","Xhamster","Tizam"],"proxyimg_disable":["Ebalovo"]},"Mirage":{"displayindex":1},"Ashdi":{"rhub":true},"Kinoukr":{"rhub":true},"VDBmovies":{"rhub":true},"VideoDB":{"rhub":true},"FanCDN":{"rhub":true},"Rezka":{"rhub":true,"scheme":"https"},"Kinotochka":{"rhub":true,"rhub_fallback":true,"rhub_streamproxy":true,"rhub_geo_disable":["RU"]},"Autoembed":{"enable":false},"Kodik":{"overridehost":"https://rc.bwa.to/lite/kodik"},"Animevost":{"rhub":true,"rhub_fallback":true,"rhub_streamproxy":true},"AnilibriaOnline":{"rhub":true},"Ebalovo":{"rhub":true},"Spankbang":{"rhub":true,"rhub_streamproxy":true,"rhub_geo_disable":["RU"]},"BongaCams":{"rhub":true,"rhub_streamproxy":true},"Chaturbate":{"rhub":true,"rhub_streamproxy":true,"rhub_geo_disable":["RU"]},"Runetki":{"rhub":true,"rhub_streamproxy":true},"Xhamster":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["RU"]},"Xnxx":{"rhub":true,"rhub_fallback":true,"rhub_streamproxy":true,"rhub_geo_disable":["RU"]},"Tizam":{"rhub":true,"rhub_fallback":true,"rhub_streamproxy":true},"Xvideos":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["RU"]},"PornHub":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"RutubeMovie":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["UA"]},"VkMovie":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"Plvideo":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true,"rhub_geo_disable":["UA"]},"CDNvideohub":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"Redheadsound":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"CDNmovies":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"AniMedia":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"Animebesst":{"rhub":true,"rhub_streamproxy":true,"rhub_fallback":true},"AnimeGo":{"enable":false}}' > /home/init.conf + +RUN echo '"typesearch":"webapi","merge":null' > /home/module/JacRed.conf + +RUN echo '[{"enable":true,"dll":"SISI.dll"},{"enable":true,"dll":"Online.dll"},{"enable":true,"initspace":"Catalog.ModInit","dll":"Catalog.dll"},{"enable":true,"initspace":"Jackett.ModInit","dll":"JacRed.dll"}]' > /home/module/manifest.json + +ENTRYPOINT ["/usr/share/dotnet/dotnet", "Lampac.dll"] diff --git a/Build/Docker/update.sh b/Build/Docker/update.sh new file mode 100644 index 0000000..4c563d4 --- /dev/null +++ b/Build/Docker/update.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +ver=$(curl -k -s https://api.github.com/repos/immisterio/Lampac/releases/latest | grep tag_name | sed s/[^0-9]//g) +upver=$(curl -k -s http://noah.lampac.sh/update/$ver.txt) + +if [[ ${#upver} -eq 8 ]]; then + curl -L -k -o update.zip http://noah.lampac.sh/update/$upver.zip + unzip -o update.zip + rm -f update.zip +fi diff --git a/Build/cloudflare/Lampac.csproj b/Build/cloudflare/Lampac.csproj new file mode 100644 index 0000000..2c6d6d1 --- /dev/null +++ b/Build/cloudflare/Lampac.csproj @@ -0,0 +1,40 @@ + + + + net9.0 + 7a9d4585-3e95-4564-a350-5fe756d1351f + Lampac + Lampac + + + + + + + + + + + + + + + + $(OutputPath)publish\ + + + + + + + + + + + + + + + + + diff --git a/Build/cloudflare/deploy.sh b/Build/cloudflare/deploy.sh new file mode 100644 index 0000000..deb5a50 --- /dev/null +++ b/Build/cloudflare/deploy.sh @@ -0,0 +1,6 @@ +curl -sSL https://dot.net/v1/dotnet-install.sh > dotnet-install.sh +chmod +x dotnet-install.sh +./dotnet-install.sh --version 9.0.310 -InstallDir ./dotnet + +chmod +x Build/cloudflare/nightlies.sh +./Build/cloudflare/nightlies.sh diff --git a/Build/cloudflare/nightlies.sh b/Build/cloudflare/nightlies.sh new file mode 100644 index 0000000..ca13442 --- /dev/null +++ b/Build/cloudflare/nightlies.sh @@ -0,0 +1,72 @@ +mkdir -p lpc/ + +cat Build/cloudflare/Lampac.csproj > Lampac/Lampac.csproj + +# Публикация проекта +./dotnet/dotnet publish Lampac -c Release + +# Целевая директория +publish_dir="Lampac/bin/Release/net9.0/publish" + +# Удаляем все папки в runtimes кроме references +for dir in "$publish_dir/runtimes"/*/; do + dirname=$(basename "$dir") + if [ "$dirname" != "references" ]; then + rm -rf "$dir" + fi +done + +# Перемещаем языковые папки в runtimes/references/ +for lang in cs de es fr it ja ko pl pt-BR ru tr zh-Hans zh-Hant; do + if [ -d "$publish_dir/$lang" ]; then + mv "$publish_dir/$lang" "$publish_dir/runtimes/references/" + fi +done + +# Копируем всё в lpc/ +cp -R "$publish_dir"/* lpc/ + +# Сборка модулей +mkdir -p lpc/module + +./dotnet/dotnet publish DLNA -c Release +cp DLNA/bin/Release/net9.0/publish/DLNA.dll lpc/module/ + +./dotnet/dotnet publish JacRed -c Release +cp JacRed/bin/Release/net9.0/publish/JacRed.dll lpc/module/ + +./dotnet/dotnet publish Merchant -c Release +cp Merchant/bin/Release/net9.0/publish/Merchant.dll lpc/module/ + +./dotnet/dotnet publish Online -c Release +cp Online/bin/Release/net9.0/publish/Online.dll lpc/module/ + +./dotnet/dotnet publish Catalog -c Release +cp Catalog/bin/Release/net9.0/publish/Catalog.dll lpc/module/ + +./dotnet/dotnet publish SISI -c Release +cp SISI/bin/Release/net9.0/publish/SISI.dll lpc/module/ + +./dotnet/dotnet publish TorrServer -c Release +cp TorrServer/bin/Release/net9.0/publish/TorrServer.dll lpc/module/ + +./dotnet/dotnet publish Tracks -c Release +cp Tracks/bin/Release/net9.0/publish/Tracks.dll lpc/module/ + +mkdir -p lpc/basemod +cp -R BaseModule/Controllers lpc/basemod/ + +cd lpc/ +rm -f Lampac.runtimeconfig.json + +curl -L -k -o cloudflare.zip "https://lampac.sh/update/cloudflare.zip?v=$(date +%s)" +unzip -o cloudflare.zip +rm -f cloudflare.zip + +python -m zipfile -c update.zip * + +cd ../ +mkdir out/ +cat Build/cloudflare/nightlies_update.sh > lpc/update.sh +cat lpc/update.sh > out/ver.sh +mv lpc out/ \ No newline at end of file diff --git a/Build/cloudflare/nightlies_update.sh b/Build/cloudflare/nightlies_update.sh new file mode 100644 index 0000000..5f71e6a --- /dev/null +++ b/Build/cloudflare/nightlies_update.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +VERSION=$1 +CUSTOM_DIR=$2 + +if [ -n "$CUSTOM_DIR" ]; then + WORK_DIR="$CUSTOM_DIR" +else + WORK_DIR="/home/lampac" +fi + +if [ -n "$VERSION" ]; then + UPDATEURI="https://${VERSION}.bwa.pages.dev/lpc/update.zip" +else + UPDATEURI="https://bwa.pages.dev/lpc/update.zip" +fi + +cd "$WORK_DIR" || { echo "Failed to change directory to $WORK_DIR. Exiting."; exit 1; } + +rm -f update.zip + +echo -e "Download $UPDATEURI \n" + +if ! curl -L -k -o update.zip "$UPDATEURI"; then + echo -e "\nFailed to download update.zip. Exiting." + exit 1 +fi +if ! unzip -t update.zip; then + echo -e "\nFailed to test update.zip. Exiting." + exit 1 +fi + +systemctl stop lampac +unzip -o update.zip +rm -f update.zip +systemctl start lampac + +echo -e "\n\nUpdate completed successfully in directory: $WORK_DIR" \ No newline at end of file diff --git a/Catalog/ApiController.cs b/Catalog/ApiController.cs new file mode 100644 index 0000000..205378b --- /dev/null +++ b/Catalog/ApiController.cs @@ -0,0 +1,214 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Catalog.Controllers +{ + public class ApiController : BaseController + { + #region catalog.js + [HttpGet] + [AllowAnonymous] + [Route("catalog.js")] + [Route("catalog/js/{token}")] + public ActionResult CatalogJS(string token) + { + var sb = new StringBuilder(FileCache.ReadAllText("plugins/catalog.js")); + + sb.Replace("{localhost}", host) + .Replace("{token}", HttpUtility.UrlEncode(token)) + .Replace("catalogs:{}", $"catalogs:{jsonCatalogs()}"); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + [HttpGet] + [Route("catalog")] + public ActionResult Index() + { + return ContentTo(jsonCatalogs()); + } + + + string jsonCatalogs() + { + var result = new JObject(); + + string dir = Path.Combine(AppContext.BaseDirectory, "catalog", "sites"); + if (!Directory.Exists(dir)) + return result.ToString(Formatting.None); + + #region sites + var sites = new List<(string key, JObject obj, int index)>(); + + foreach (var file in Directory.GetFiles(dir, "*.yaml")) + { + try + { + var site = Path.GetFileNameWithoutExtension(file); + if (string.IsNullOrEmpty(site)) + continue; + + var init = ModInit.goInit(site); + if (init == null || !init.enable || init.menu == null || init.hide) + continue; + + var siteObj = new JObject(); + + foreach (var menuItem in init.menu) + { + if (menuItem?.categories == null || menuItem.categories.Count == 0) + continue; + + foreach (var cat in menuItem.categories) + { + string catName = cat.Key; + string catCode = cat.Value; + + if (!(siteObj[catName] is JObject catObj)) + { + catObj = new JObject(); + + if (init.search != null) + siteObj["search"] = $"/catalog/list?plugin={HttpUtility.UrlEncode(menuItem.catalog ?? site)}"; + + siteObj["search_lazy"] = init.search_lazy; + + if (!string.IsNullOrEmpty(init.catalog_key)) + siteObj["catalog_key"] = init.catalog_key; + + if (!string.IsNullOrEmpty(menuItem.defaultName)) + siteObj["defaultName"] = menuItem.defaultName; + + siteObj[catName] = catObj; + } + + string baseUrl = $"/catalog/list?plugin={HttpUtility.UrlEncode(menuItem.catalog ?? site)}&cat={HttpUtility.UrlEncode(catCode)}"; + + bool addBaseEntry = true; + if (menuItem.format != null) + { + if (!menuItem.format.ContainsKey("-")) + addBaseEntry = false; + } + + if (addBaseEntry) + { + if (catObj[catName] == null) + catObj[catName] = baseUrl; + } + + if (menuItem.sort != null) + { + foreach (var s in menuItem.sort) + { + string sortName = s.Key; + string sortCode = s.Value; + if (string.IsNullOrEmpty(sortName) || string.IsNullOrEmpty(sortCode)) + continue; + + string sortUrl = baseUrl + "&sort=" + HttpUtility.UrlEncode(sortCode); + if (catObj[sortName] == null) + catObj[sortName] = sortUrl; + } + } + } + } + + string siteKey = !string.IsNullOrEmpty(init.plugin) ? init.plugin : init.displayname ?? site; + + int idx = init.displayindex; + if (idx == 0) + idx = int.MaxValue - sites.Count; + + sites.Add((siteKey, siteObj, idx)); + } + catch { } + } + #endregion + + #region result + foreach (var s in sites.OrderBy(x => x.index)) + { + result[s.key] = new JObject(); + + if (s.obj.ContainsKey("search")) + result[s.key]["search"] = s.obj["search"]; + + result[s.key]["search_lazy"] = s.obj["search_lazy"]; + + string catalog_key = s.obj.ContainsKey("catalog_key") ? s.obj["catalog_key"]?.ToString() : null; + string defaultName = s.obj.ContainsKey("defaultName") ? s.obj["defaultName"]?.ToString() : null; + + var menu = new JObject(); + var main = new JObject(); + + foreach (var prop in s.obj.Properties()) + { + if (!(prop.Value is JObject catObj)) + continue; + + foreach (var inner in catObj.Properties()) + { + string pname = prop.Name; + if (pname.StartsWith("[")) + pname = prop.Name.Split(']')[1].Trim(); + + if (pname != inner.Name) + main[$"{pname} • {inner.Name.ToLower()}"] = inner.Value; + else + main[pname] = inner.Value; + + if (!menu.ContainsKey(pname) || (catalog_key != null && catalog_key == inner.Name)) + menu[pname] = inner.Value; + } + + var categoryMap = new Dictionary + { + { "Фильмы", "movie" }, + { "Сериалы", "tv" }, + { "Мультфильмы", "cartoons" }, + { "Аниме", "anime" }, + { "Релизы", "relise" } + }; + + string targetCat, targetName = null; + + if (categoryMap.TryGetValue(prop.Name, out targetCat)) + targetName = prop.Name; + + if (prop.Name.StartsWith("[")) + { + targetCat = prop.Name.Split(']')[0].Trim('['); + targetName = prop.Name.Split(']')[1]; + } + + if (!string.IsNullOrEmpty(targetName) && !string.IsNullOrEmpty(targetCat)) + { + var targetObj = new JObject(); + + foreach (var inner in catObj.Properties()) + { + if (targetName.Trim() != inner.Name) + targetObj[inner.Name] = inner.Value; + else + targetObj[defaultName ?? inner.Name] = inner.Value; + } + + if (targetObj.HasValues) + result[s.key][targetCat.Trim()] = targetObj; + } + } + + if (menu.HasValues) + result[s.key]["menu"] = menu; + + if (main.HasValues) + result[s.key]["main"] = main; + } + #endregion + + return result.ToString(Formatting.None); + } + } +} diff --git a/Catalog/CardController.cs b/Catalog/CardController.cs new file mode 100644 index 0000000..91c4e23 --- /dev/null +++ b/Catalog/CardController.cs @@ -0,0 +1,397 @@ +using Microsoft.AspNetCore.Mvc; +using Shared.Engine.Utilities; +using Shared.PlaywrightCore; +using System.Net.Http; + +namespace Catalog.Controllers +{ + public class CardController : BaseController + { + [HttpGet] + [Route("catalog/card")] + public async Task Index(string plugin, string uri, string type) + { + var init = ModInit.goInit(plugin)?.Clone(); + if (init == null || !init.enable) + return BadRequest("init not found"); + + var rch = new RchClient(HttpContext, host, init, requestInfo); + if (rch.IsNotConnected()) + rch.Disabled(); + + var proxyManager = new ProxyManager(init, rch); + var proxy = proxyManager.BaseGet(); + + string memKey = $"catalog:card:{plugin}:{uri}"; + + return await InvkSemaphore(memKey, rch, async () => + { + if (!hybridCache.TryGetValue(memKey, out JObject jo, inmemory: false)) + { + string url = $"{init.host}/{uri}"; + var headers = httpHeaders(init); + + if (init.args != null) + url = url.Contains("?") ? $"{url}&{init.args}" : $"{url}?{init.args}"; + + if (init.card_parse.initUrl != null) + url = CSharpEval.Execute(init.card_parse.initUrl, new CatalogInitUrlCard(init.host, init.args, uri, HttpContext.Request.Query, type)); + + if (init.card_parse.initHeader != null) + headers = CSharpEval.Execute>(init.card_parse.initHeader, new CatalogInitHeader(url, headers)); + + reset: + + string html = null; + + if (!string.IsNullOrEmpty(init.card_parse.postData)) + { + string mediaType = init.card_parse.postData.StartsWith("{") || init.card_parse.postData.StartsWith("[") ? "application/json" : "application/x-www-form-urlencoded"; + var httpdata = new StringContent(init.card_parse.postData, Encoding.UTF8, mediaType); + + html = rch.enable + ? await rch.Post(url, init.card_parse.postData, headers, useDefaultHeaders: init.useDefaultHeaders) + : await Http.Post(url, httpdata, headers: headers, proxy: proxy.proxy, timeoutSeconds: init.timeout, httpversion: init.httpversion, useDefaultHeaders: init.useDefaultHeaders); + } + else + { + html = rch.enable + ? await rch.Get(url, headers, useDefaultHeaders: init.useDefaultHeaders) + : init.priorityBrowser == "playwright" ? await PlaywrightBrowser.Get(init, url, headers, proxy.data, cookies: init.cookies) + : await Http.Get(url, headers: headers, proxy: proxy.proxy, timeoutSeconds: init.timeout, httpversion: init.httpversion, useDefaultHeaders: init.useDefaultHeaders); + } + + if (html == null) + { + if (ModInit.IsRhubFallback(init)) + goto reset; + + proxyManager.Refresh(); + + return BadRequest("html"); + } + + proxyManager.Success(); + + var parse = init.card_parse; + bool? jsonPath = parse.jsonPath; + if (jsonPath == null) + jsonPath = init.jsonPath; + + #region parse doc/json + HtmlNode node = null; + JToken json = null; + + if (jsonPath == true) + { + try + { + json = JToken.Parse(html); + + if (!string.IsNullOrEmpty(parse.node)) + { + json = json.SelectToken(parse.node); + if (json == null) + return BadRequest("parse.node"); + } + } + catch + { + json = null; + return BadRequest("json"); + } + } + else + { + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + node = doc.DocumentNode; + } + #endregion + + #region name / original_name / year + string name; + string original_name; + string year; + + if (jsonPath == true) + { + name = ModInit.nodeValue(json, parse.name, host)?.ToString(); + original_name = ModInit.nodeValue(json, parse.original_name, host)?.ToString(); + year = ModInit.nodeValue(json, parse.year, host)?.ToString(); + } + else + { + name = ModInit.nodeValue(node, parse.name, host)?.ToString(); + original_name = ModInit.nodeValue(node, parse.original_name, host)?.ToString(); + year = ModInit.nodeValue(node, parse.year, host)?.ToString(); + } + #endregion + + #region img + string img = jsonPath == true + ? ModInit.nodeValue(json, parse.image, host)?.ToString() + : ModInit.nodeValue(node, parse.image, host)?.ToString(); + + if (img != null) + { + img = img.Replace("&", "&").Replace("\\", ""); + + if (img.StartsWith("../")) + img = $"{init.host}/{img.Replace("../", "")}"; + else if (img.StartsWith("//")) + img = $"https:{img}"; + else if (img.StartsWith("/")) + img = init.host + img; + else if (!img.StartsWith("http")) + img = $"{init.host}/{img}"; + } + #endregion + + jo = new JObject() + { + ["id"] = uri.Trim(), + ["img"] = PosterApi.Size(host, img), + + ["vote_average"] = 0, + ["genres"] = new JArray(), + ["production_countries"] = new JArray(), + ["production_companies"] = new JArray() + }; + + string overview = jsonPath == true + ? ModInit.nodeValue(json, parse.description, host)?.ToString() + : ModInit.nodeValue(node, parse.description, host)?.ToString(); + + if (!string.IsNullOrEmpty(overview)) + jo["overview"] = overview; + + if (type == "tv") + { + jo["first_air_date"] = year; + jo["name"] = name; + + if (!string.IsNullOrEmpty(original_name)) + jo["original_name"] = original_name; + } + else + { + jo["release_date"] = year; + jo["title"] = name; + + if (!string.IsNullOrEmpty(original_name)) + jo["original_title"] = original_name; + } + + #region card_args + if (init.card_args != null) + { + foreach (var arg in init.card_args) + { + object val = jsonPath == true + ? ModInit.nodeValue(json, arg, host) + : ModInit.nodeValue(node, arg, host); + + ModInit.setArgsValue(arg, val, jo); + } + } + #endregion + + if (init.tmdb_injects != null && init.tmdb_injects.Length > 0) + await Injects(year, jo, init.tmdb_injects); + + if (!jo.ContainsKey("tagline") && !string.IsNullOrEmpty(original_name)) + jo["tagline"] = original_name; + + hybridCache.Set(memKey, jo, cacheTimeBase(init.cache_time, init: init), inmemory: false); + } + + return ContentTo(JsonConvertPool.SerializeObject(jo)); + }); + } + + + #region TMDB Injects + static readonly string[] defaultInjectskeys = + [ + "imdb_id", + "external_ids", + "backdrop_path", + "created_by", + "genres", + "production_companies", + "production_countries", + "content_ratings", + "episode_run_time", + "languages", + "number_of_episodes", + "number_of_seasons", + "last_episode_to_air", + "origin_country", + "original_language", + "status", + "networks", + "seasons", + "type", + "budget", + "spoken_languages", + "alternative_titles", + "keywords", + + // &append_to_response= + "videos", + "credits", + "recommendations", + "similar", + ]; + + static readonly string[] addEmptykeys = + [ + "tagline", + "overview", + "first_air_date", + "last_air_date", + "release_date", + "runtime" + ]; + + async Task Injects(string year, JObject jo, string[] keys) + { + if (!jo.ContainsKey("imdb_id") && !jo.ContainsKey("original_title") && !jo.ContainsKey("original_name")) + return; + + if (keys.Length == 1 && keys[0] == "default") + keys = defaultInjectskeys; + + #region Поиск карточки в TMDB + string imdbId = null; + if (jo.ContainsKey("imdb_id")) + imdbId = jo["imdb_id"]?.ToString(); + + var header = HeadersModel.Init(("localrequest", AppInit.rootPasswd)); + + long id = 0; + string cat = string.Empty; + + if (!string.IsNullOrWhiteSpace(imdbId) && imdbId.StartsWith("tt")) + { + var find = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/find/{imdbId}?external_source=imdb_id&api_key={AppInit.conf.tmdb.api_key}", timeoutSeconds: 5, headers: header); + if (find != null) + { + foreach (string key in new string[] { "movie_results", "tv_results" }) + { + if (find.ContainsKey(key)) + { + var movies = find[key] as JArray; + if (movies != null && movies.Count > 0) + { + id = movies[0].Value("id"); + cat = key == "movie_results" ? "movie" : "tv"; + break; + } + } + } + } + } + else if (jo.ContainsKey("original_title") || jo.ContainsKey("original_name")) + { + string type = jo.ContainsKey("original_title") ? "movie" : "tv"; + string originalTitle = jo.Value(type == "movie" ? "original_title" : "original_name"); + + if (!string.IsNullOrEmpty(originalTitle) && int.TryParse(year.Split("-")[0], out int _year) && _year > 0) + { + var searchMovie = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/search/{type}?query={HttpUtility.UrlEncode(originalTitle)}&api_key={AppInit.conf.tmdb.api_key}", timeoutSeconds: 5, headers: header); + if (searchMovie != null && searchMovie.ContainsKey("results")) + { + var results = searchMovie["results"] as JArray; + if (results != null && results.Count > 0) + { + long foundId = 0; + for (int i = 0; i < results.Count; i++) + { + var item = results[i] as JObject; + if (item == null) + continue; + + string date = item.Value("release_date") ?? item.Value("first_air_date"); + if (string.IsNullOrEmpty(date)) + continue; + + // date is usually in format YYYY-MM-DD, take first 4 chars + string yearStr = date.Length >= 4 ? date.Substring(0, 4) : date; + if (int.TryParse(yearStr, out int itemYear) && itemYear == _year) + { + string _s1 = StringConvert.SearchName(originalTitle); + string _s2 = StringConvert.SearchName(item.Value(type == "movie" ? "original_title" : "original_name")); + + if (!string.IsNullOrEmpty(_s1) && !string.IsNullOrEmpty(_s2) && _s1 == _s2) + { + foundId = item.Value("id"); + break; + } + } + } + + if (foundId != 0) + { + id = foundId; + cat = type; + } + } + } + } + } + #endregion + + if (id == 0) + return; + + string append = "content_ratings,release_dates,external_ids,keywords,alternative_titles,videos,credits,recommendations,similar"; + var result = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/{cat}/{id}?api_key={AppInit.conf.tmdb.api_key}&append_to_response={append}&language=ru", timeoutSeconds: 5, headers: header); + if (result == null) + return; + + foreach (string key in keys) + { + if (key is "videos" or "recommendations" or "similar") + { + if (result.ContainsKey(key) && result[key] is JObject _jo && _jo.ContainsKey("results")) + jo[key] = _jo["results"]; + } + else if (result.ContainsKey(key)) + { + jo[key] = result[key]; + } + } + + if (result.ContainsKey("id")) + jo["tmdb_id"] = result["id"]; + + if (!jo.ContainsKey("imdb_id") && result.ContainsKey("external_ids") && result["external_ids"] is JObject extIds && extIds.ContainsKey("imdb_id")) + jo["imdb_id"] = extIds["imdb_id"]; + + foreach (string key in addEmptykeys) + { + if (!jo.ContainsKey(key) && result.ContainsKey(key)) + { + var tok = result[key]; + if (tok == null) + continue; + + if (tok.Type == JTokenType.String) + { + var str = tok.Value(); + if (!string.IsNullOrWhiteSpace(str)) + jo[key] = str; + } + else + { + jo[key] = tok; + } + } + } + } + #endregion + } +} diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..a644285 --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + library + true + + + + + + + diff --git a/Catalog/GlobalUsings.cs b/Catalog/GlobalUsings.cs new file mode 100644 index 0000000..1043555 --- /dev/null +++ b/Catalog/GlobalUsings.cs @@ -0,0 +1,18 @@ +global using System; +global using System.Threading.Tasks; +global using System.Collections.Generic; +global using System.Text.RegularExpressions; +global using System.IO; +global using System.Linq; +global using Shared; +global using Shared.Models; +global using Shared.Engine; +global using Shared.Models.Base; +global using Shared.Models.Catalog; +global using Shared.Models.CSharpGlobals; +global using HtmlAgilityPack; +global using Newtonsoft.Json; +global using Newtonsoft.Json.Linq; +global using Microsoft.CodeAnalysis.Scripting; +global using System.Text; +global using System.Web; \ No newline at end of file diff --git a/Catalog/ListController.cs b/Catalog/ListController.cs new file mode 100644 index 0000000..16f3ef6 --- /dev/null +++ b/Catalog/ListController.cs @@ -0,0 +1,547 @@ +using Microsoft.AspNetCore.Mvc; +using Shared.PlaywrightCore; +using System.Net.Http; + +namespace Catalog.Controllers +{ + public class ListController : BaseController + { + [HttpGet] + [Route("catalog/list")] + async public ValueTask Index(string query, string plugin, string cat, string sort, int page = 1) + { + var init = ModInit.goInit(plugin)?.Clone(); + if (init == null || !init.enable) + return BadRequest("init not found"); + + if (!string.IsNullOrEmpty(query) && string.IsNullOrEmpty(init.search?.uri)) + return BadRequest("search disable"); + + var rch = new RchClient(HttpContext, host, init, requestInfo); + if (rch.IsNotConnected()) + rch.Disabled(); + + var proxyManager = new ProxyManager(init, rch); + var proxy = proxyManager.BaseGet(); + + string search = query; + string memKey = $"catalog:{plugin}:{search}:{sort}:{cat}:{page}"; + + return await InvkSemaphore(memKey, rch, async () => + { + if (!hybridCache.TryGetValue(memKey, out (List playlists, int total_pages) cache, inmemory: false)) + { + #region contentParse + var contentParse = init.list?.contentParse ?? init.content; + + if (!string.IsNullOrEmpty(search) && init.search?.contentParse != null) + contentParse = init.search.contentParse; + #endregion + + #region html + var headers = httpHeaders(init); + var parse = init.list; + + string url = $"{init.host}/{(page == 1 && init.list?.firstpage != null ? init.list?.firstpage : init.list?.uri)}"; + string data = init.list?.postData; + + if (!string.IsNullOrEmpty(search)) + { + string uri = page == 1 && init.search?.firstpage != null ? init.search.firstpage : init.search?.uri; + url = $"{init.host}/{uri}".Replace("{search}", HttpUtility.UrlEncode(search)); + + data = init.search?.postData?.Replace("{search}", HttpUtility.UrlEncode(search)); + parse = init.search; + } + else if (!string.IsNullOrEmpty(cat)) + { + var menu = init.menu.FirstOrDefault(i => i.categories.Values.Contains(cat)); + if (menu == null) + return BadRequest("menu"); + + string getFormat(string key) + { + if (menu.format.TryGetValue(key, out string _f)) + return _f; + + return string.Empty; + } + + string eval = (cat != null && sort != null) ? getFormat("sort") : getFormat("-"); + if (!string.IsNullOrEmpty(eval)) + { + if (!eval.Contains("$\"") && eval.Contains("{") && eval.Contains("}")) + eval = $"return $\"{eval}\";"; + + url = CSharpEval.BaseExecute(eval, new CatalogGlobalsMenuRoute(init.host, plugin, init.args, url, search, cat, sort, HttpContext.Request.Query, page)); + } + + if (!url.StartsWith("http")) + url = $"{init.host}/{url}"; + } + + if (init.args != null) + url = url.Contains("?") ? $"{url}&{init.args}" : $"{url}?{init.args}"; + + if (parse?.initUrl != null) + url = CSharpEval.Execute(parse.initUrl, new CatalogGlobalsMenuRoute(init.host, plugin, init.args, url, search, cat, sort, HttpContext.Request.Query, page)); + + if (parse?.initHeader != null) + headers = CSharpEval.Execute>(parse.initHeader, new CatalogInitHeader(url, headers)); + + reset: + string html = null; + + if (!string.IsNullOrEmpty(data)) + { + string mediaType = data.StartsWith("{") || data.StartsWith("[") ? "application/json" : "application/x-www-form-urlencoded"; + var httpdata = new StringContent(data, Encoding.UTF8, mediaType); + + html = rch.enable + ? await rch.Post(url.Replace("{page}", page.ToString()), data, headers, useDefaultHeaders: init.useDefaultHeaders) + : await Http.Post(url.Replace("{page}", page.ToString()), httpdata, headers: headers, proxy: proxy.proxy, timeoutSeconds: init.timeout, httpversion: init.httpversion, useDefaultHeaders: init.useDefaultHeaders); + } + else + { + html = rch.enable + ? await rch.Get(url.Replace("{page}", page.ToString()), headers, useDefaultHeaders: init.useDefaultHeaders) + : init.priorityBrowser == "playwright" ? await PlaywrightBrowser.Get(init, url.Replace("{page}", page.ToString()), headers, proxy.data, cookies: init.cookies) + : await Http.Get(url.Replace("{page}", page.ToString()), headers: headers, proxy: proxy.proxy, timeoutSeconds: init.timeout, httpversion: init.httpversion, useDefaultHeaders: init.useDefaultHeaders); + } + #endregion + + bool? jsonPath = contentParse.jsonPath; + if (jsonPath == null) + jsonPath = init.jsonPath; + + #region parse doc/json + HtmlDocument doc = null; + JToken json = null; + + if (jsonPath == true) + { + try + { + json = JToken.Parse(html); + } + catch + { + json = null; + } + } + else + { + doc = new HtmlDocument(); + + if (html != null) + doc.LoadHtml(html); + } + #endregion + + cache.playlists = jsonPath == true + ? goPlaylistJson(cat, json, requestInfo, host, contentParse, init, html, plugin) + : goPlaylist(cat, doc, requestInfo, host, contentParse, init, html, plugin); + + if (cache.playlists == null || cache.playlists.Count == 0) + { + if (ModInit.IsRhubFallback(init)) + goto reset; + + proxyManager.Refresh(); + + return BadRequest("playlists"); + } + + if (contentParse.total_pages != null) + { + string _p = jsonPath == true + ? ModInit.nodeValue(json, contentParse.total_pages, host)?.ToString() ?? "" + : ModInit.nodeValue(doc.DocumentNode, contentParse.total_pages, host)?.ToString() ?? ""; + + if (int.TryParse(_p, out int _pages) && _pages > 0) + cache.total_pages = _pages; + } + + proxyManager.Success(); + + hybridCache.Set(memKey, cache, cacheTimeBase(init.cache_time, init: init), inmemory: false); + } + + #region total_pages + int? total_pages = init.list?.total_pages ?? 0; + + if (search != null && init.search != null) + total_pages = init.search.total_pages; + + if (total_pages == 0) + total_pages = cache.total_pages; + #endregion + + #region next_page + bool? next_page = null; + + if (search != null) + { + if (init.search != null && init.search.count_page > 0 && cache.playlists.Count >= init.search.count_page) + next_page = true; + } + else + { + if (init.list != null && init.list.count_page > 0 && cache.playlists.Count >= init.list.count_page) + next_page = true; + } + + if (next_page == true && total_pages == 0) + total_pages = null; + #endregion + + #region results + var results = new JArray(); + + foreach (var pl in cache.playlists) + { + var jo = new JObject() + { + ["id"] = pl.id, + ["img"] = pl.img + }; + + if (pl.is_serial) + { + jo["first_air_date"] = pl.year; + jo["name"] = pl.title; + jo["original_name"] = string.IsNullOrWhiteSpace(pl.original_title) ? pl.title : pl.original_title; + } + else + { + jo["release_date"] = pl.year; + jo["title"] = pl.title; + jo["original_title"] = string.IsNullOrWhiteSpace(pl.original_title) ? pl.title : pl.original_title; + } + + if (pl.args != null) + { + foreach (var a in pl.args) + jo[a.Key] = JToken.FromObject(a.Value); + } + + results.Add(jo); + } + #endregion + + return ContentTo(JsonConvert.SerializeObject(new + { + page, + results, + total_pages, + next_page + + }, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore + })); + }); + } + + + #region goPlaylistJson + static List goPlaylistJson(string cat, JToken json, RequestModel requestInfo, string host, ContentParseSettings parse, CatalogSettings init, string html, string plugin) + { + if (parse == null || json == null) + return null; + + if (init.debug) + Console.WriteLine(html); + + string eval = parse.eval; + if (!string.IsNullOrEmpty(eval) && eval.EndsWith(".cs")) + eval = FileCache.ReadAllText($"catalog/sites/{eval}"); + + if (string.IsNullOrEmpty(parse.nodes)) + { + if (string.IsNullOrEmpty(eval)) + return null; + + var options = ScriptOptions.Default + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")) + .AddImports("Shared") + .AddImports("Shared.Models") + .AddImports("Shared.Engine") + .AddReferences(CSharpEval.ReferenceFromFile("Newtonsoft.Json.dll")) + .AddImports("Newtonsoft.Json") + .AddImports("Newtonsoft.Json.Linq"); + + return CSharpEval.Execute>(eval, new CatalogPlaylistJson(init, plugin, host, html, json, new List()), options); + } + + var nodes = json.SelectTokens(parse.nodes)?.ToList(); + if (nodes == null || nodes.Count == 0) + return null; + + var playlists = new List(nodes.Count); + + foreach (var node in nodes) + { + string name = ModInit.nodeValue(node, parse.name, host)?.ToString(); + string original_name = ModInit.nodeValue(node, parse.original_name, host)?.ToString(); + string href = ModInit.nodeValue(node, parse.href, host)?.ToString(); + string img = ModInit.nodeValue(node, parse.image, host)?.ToString(); + string year = ModInit.nodeValue(node, parse.year, host)?.ToString(); + + if (init.debug) + Console.WriteLine($"\n\nname: {name}\noriginal_name: {original_name}\nhref: {href}\nimg: {img}\nyear: {year}\n\n{node.ToString(Formatting.None)}"); + + if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(href)) + { + #region href + if (href.StartsWith("../")) + href = href.Replace("../", ""); + else if (href.StartsWith("//")) + href = Regex.Replace(href, "//[^/]+/", ""); + else if (href.StartsWith("http")) + href = Regex.Replace(href, "https?://[^/]+/", ""); + else if (href.StartsWith("/")) + href = href.Substring(1); + #endregion + + #region img + if (img != null) + { + img = img.Replace("&", "&").Replace("\\", ""); + + if (img.StartsWith("../")) + img = $"{init.host}/{img.Replace("../", "")}"; + else if (img.StartsWith("//")) + img = $"https:{img}"; + else if (img.StartsWith("/")) + img = init.host + img; + else if (!img.StartsWith("http")) + img = $"{init.host}/{img}"; + } + #endregion + + if (!init.ignore_no_picture && string.IsNullOrEmpty(img)) + continue; + + string clearText(string text) + { + if (string.IsNullOrEmpty(text)) + return text; + + text = text.Replace(" ", ""); + text = Regex.Replace(text, "<[^>]+>", ""); + text = HttpUtility.HtmlDecode(text); + return text.Trim(); + } + + #region is_serial + bool? is_serial = null; + + if (cat != null) + { + if (init.movie_cats != null && init.movie_cats.Contains(cat)) + is_serial = false; + else if (init.serial_cats != null && init.serial_cats.Contains(cat)) + is_serial = true; + } + + if (is_serial == null && parse.serial_regex != null) + is_serial = Regex.IsMatch(node.ToString(Formatting.None), parse.serial_regex, RegexOptions.IgnoreCase); + + if (is_serial == null && parse.serial_key != null) + { + if (ModInit.nodeValue(node, parse.serial_key, host) != null) + is_serial = true; + } + #endregion + + var pl = new PlaylistItem() + { + id = href, + title = clearText(name), + original_title = clearText(original_name), + img = PosterApi.Size(host, img), + year = clearText(year), + is_serial = is_serial == true + }; + + if (parse.args != null) + { + foreach (var arg in parse.args) + { + if (pl.args == null) + pl.args = new JObject(); + + object val = ModInit.nodeValue(node, arg, host); + ModInit.setArgsValue(arg, val, pl.args); + } + } + + if (eval != null) + { + var options = ScriptOptions.Default + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")) + .AddImports("Shared") + .AddImports("Shared.Models") + .AddImports("Shared.Engine") + .AddReferences(CSharpEval.ReferenceFromFile("Newtonsoft.Json.dll")) + .AddImports("Newtonsoft.Json") + .AddImports("Newtonsoft.Json.Linq"); + + pl = CSharpEval.Execute(eval, new CatalogChangePlaylisJson(init, plugin, host, html, nodes, pl, node), options); + } + + if (pl != null) + playlists.Add(pl); + } + } + + return playlists; + } + #endregion + + #region goPlaylist + static List goPlaylist(string cat, HtmlDocument doc, RequestModel requestInfo, string host, ContentParseSettings parse, CatalogSettings init, string html, string plugin) + { + if (parse == null || string.IsNullOrEmpty(html)) + return null; + + if (init.debug) + Console.WriteLine(html); + + string eval = parse.eval; + if (!string.IsNullOrEmpty(eval) && eval.EndsWith(".cs")) + eval = FileCache.ReadAllText($"catalog/sites/{eval}"); + + if (string.IsNullOrEmpty(parse.nodes)) + { + if (string.IsNullOrEmpty(eval)) + return null; + + var options = ScriptOptions.Default + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")) + .AddImports("Shared") + .AddImports("Shared.Models") + .AddImports("Shared.Engine") + .AddReferences(CSharpEval.ReferenceFromFile("HtmlAgilityPack.dll")) + .AddImports("HtmlAgilityPack"); + + return CSharpEval.Execute>(eval, new CatalogPlaylist(init, plugin, host, html, doc, new List()), options); + } + + var nodes = doc.DocumentNode.SelectNodes(parse.nodes); + if (nodes == null || nodes.Count == 0) + return null; + + var playlists = new List(nodes.Count); + + foreach (var node in nodes) + { + string name = ModInit.nodeValue(node, parse.name, host)?.ToString(); + string original_name = ModInit.nodeValue(node, parse.original_name, host)?.ToString(); + string href = ModInit.nodeValue(node, parse.href, host)?.ToString(); + string img = ModInit.nodeValue(node, parse.image, host)?.ToString(); + string year = ModInit.nodeValue(node, parse.year, host)?.ToString(); + + if (init.debug) + Console.WriteLine($"\n\nname: {name}\noriginal_name: {original_name}\nhref: {href}\nimg: {img}\nyear: {year}\n\n{node.OuterHtml}"); + + if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(href)) + { + #region href + if (href.StartsWith("../")) + href = href.Replace("../", ""); + else if (href.StartsWith("//")) + href = Regex.Replace(href, "//[^/]+/", ""); + else if (href.StartsWith("http")) + href = Regex.Replace(href, "https?://[^/]+/", ""); + else if (href.StartsWith("/")) + href = href.Substring(1); + #endregion + + #region img + if (img != null) + { + img = img.Replace("&", "&").Replace("\\", ""); + + if (img.StartsWith("../")) + img = $"{init.host}/{img.Replace("../", "")}"; + else if (img.StartsWith("//")) + img = $"https:{img}"; + else if (img.StartsWith("/")) + img = init.host + img; + else if (!img.StartsWith("http")) + img = $"{init.host}/{img}"; + } + #endregion + + if (!init.ignore_no_picture && string.IsNullOrEmpty(img)) + continue; + + #region is_serial + bool? is_serial = null; + + if (cat != null) + { + if (init.movie_cats != null && init.movie_cats.Contains(cat)) + is_serial = false; + else if (init.serial_cats != null && init.serial_cats.Contains(cat)) + is_serial = true; + } + + if (is_serial == null && parse.serial_regex != null) + is_serial = Regex.IsMatch(node.OuterHtml, parse.serial_regex, RegexOptions.IgnoreCase); + + if (is_serial == null && parse.serial_key != null) + { + if (ModInit.nodeValue(node, parse.serial_key, host) != null) + is_serial = true; + } + #endregion + + var pl = new PlaylistItem() + { + id = href, + title = ModInit.clearText(name), + original_title = ModInit.clearText(original_name), + img = PosterApi.Size(host, img), + year = ModInit.clearText(year), + is_serial = is_serial == true + }; + + if (parse.args != null) + { + foreach (var arg in parse.args) + { + if (pl.args == null) + pl.args = new JObject(); + + object val = ModInit.nodeValue(node, arg, host); + ModInit.setArgsValue(arg, val, pl.args); + } + } + + if (eval != null) + { + var options = ScriptOptions.Default + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")) + .AddImports("Shared") + .AddImports("Shared.Models") + .AddImports("Shared.Engine") + .AddReferences(CSharpEval.ReferenceFromFile("HtmlAgilityPack.dll")) + .AddImports("HtmlAgilityPack"); + + pl = CSharpEval.Execute(eval, new CatalogChangePlaylis(init, plugin, host, html, nodes, pl, node), options); + } + + if (pl != null) + playlists.Add(pl); + } + } + + return playlists; + } + #endregion + } +} diff --git a/Catalog/ModInit.cs b/Catalog/ModInit.cs new file mode 100644 index 0000000..7330113 --- /dev/null +++ b/Catalog/ModInit.cs @@ -0,0 +1,401 @@ +using System.Globalization; +using YamlDotNet.Serialization; + +namespace Catalog +{ + public class ModInit + { + public static void loaded() + { + } + + #region goInit + public static CatalogSettings goInit(string site) + { + if (string.IsNullOrEmpty(site)) + return null; + + site = site.ToLowerAndTrim(); + site = Regex.Replace(site, "[^a-z0-9\\-]", "", RegexOptions.IgnoreCase); + + var hybridCache = IHybridCache.Get(null); + + string memKey = $"catalog:goInit:{site}"; + if (!hybridCache.TryGetValue(memKey, out CatalogSettings init)) + { + // Если файл не найден по имени, пробуем найти по displayname в *.yaml + if (!File.Exists($"catalog/sites/{site}.yaml")) + { + string found = FindSiteByDisplayName(site); + if (string.IsNullOrEmpty(found)) + return null; + + site = found; + } + + var deserializer = new DeserializerBuilder().Build(); + + // Чтение основного YAML-файла + string yaml = File.ReadAllText($"catalog/sites/{site}.yaml"); + var target = deserializer.Deserialize>(yaml); + + foreach (string y in new string[] { "_", site }) + { + if (File.Exists($"catalog/override/{y}.yaml")) + { + // Чтение пользовательского YAML-файла + string myYaml = File.ReadAllText($"catalog/override/{y}.yaml"); + var mySource = deserializer.Deserialize>(myYaml); + + // Объединение словарей + foreach (var property in mySource) + { + if (!target.ContainsKey(property.Key)) + { + target[property.Key] = property.Value; + continue; + } + + if (property.Value is IDictionary sourceDict && + target[property.Key] is IDictionary targetDict) + { + // Рекурсивное объединение вложенных словарей + foreach (var item in sourceDict) + targetDict[item.Key] = item.Value; + } + else + { + target[property.Key] = property.Value; + } + } + } + } + + // Преобразование словаря в объект CatalogSettings + var serializer = new SerializerBuilder().Build(); + + var yamlResult = serializer.Serialize(target); + init = deserializer.Deserialize(yamlResult); + + if (string.IsNullOrEmpty(init.plugin)) + init.plugin = init.displayname; + + if (!init.debug || !AppInit.conf.multiaccess) + hybridCache.Set(memKey, init, DateTime.Now.AddMinutes(1), inmemory: true); + } + + return init; + } + #endregion + + #region FindSiteByDisplayName + static string FindSiteByDisplayName(string site) + { + var deserializer = new DeserializerBuilder().Build(); + + foreach (var folder in new[] { "catalog/sites", "catalog/override" }) + { + if (!Directory.Exists(folder)) + continue; + + foreach (var file in Directory.EnumerateFiles(folder, "*.yaml")) + { + try + { + var yaml = File.ReadAllText(file); + var dict = deserializer.Deserialize>(yaml); + if (dict != null && dict.TryGetValue("displayname", out var dnObj) && dnObj != null) + { + var dn = dnObj.ToString().ToLowerAndTrim(); + if (dn == site) + return Path.GetFileNameWithoutExtension(file); + } + } + catch { } + } + } + + return null; + } + #endregion + + #region IsRhubFallback + public static bool IsRhubFallback(BaseSettings init) + { + if (init.rhub && init.rhub_fallback) + { + init.rhub = false; + return true; + } + + return false; + } + #endregion + + + #region nodeValue - HtmlNode + public static object nodeValue(HtmlNode node, SingleNodeSettings nd, string host) + { + string value = null; + + if (nd != null) + { + if (string.IsNullOrEmpty(nd.node) && (!string.IsNullOrEmpty(nd.attribute) || nd.attributes != null)) + { + if (nd.attributes != null) + { + foreach (var attr in nd.attributes) + { + var attrValue = node.GetAttributeValue(attr, null); + if (!string.IsNullOrEmpty(attrValue)) + { + value = attrValue; + break; + } + } + } + else + { + if ("innerhtml".Equals(nd.attribute, StringComparison.OrdinalIgnoreCase)) + value = node.InnerHtml; + else if ("outerhtml".Equals(nd.attribute, StringComparison.OrdinalIgnoreCase)) + value = node.OuterHtml; + else + value = node.GetAttributeValue(nd.attribute, null); + } + } + else + { + var inNode = node.SelectSingleNode(nd.node); + if (inNode != null) + { + if (nd.attributes != null) + { + foreach (var attr in nd.attributes) + { + var attrValue = inNode.GetAttributeValue(attr, null); + if (!string.IsNullOrEmpty(attrValue)) + { + value = attrValue; + break; + } + } + } + else + { + if (!string.IsNullOrEmpty(nd.attribute)) + { + if ("innerhtml".Equals(nd.attribute, StringComparison.OrdinalIgnoreCase)) + value = inNode.InnerHtml; + else if ("outerhtml".Equals(nd.attribute, StringComparison.OrdinalIgnoreCase)) + value = inNode.OuterHtml; + else + value = inNode.GetAttributeValue(nd.attribute, null)?.Trim(); + } + else + { + value = inNode.InnerText?.Trim(); + } + } + } + } + } + + if (string.IsNullOrEmpty(value)) + return null; + + if (nd.format != null) + { + var options = ScriptOptions.Default + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")) + .AddImports("Shared") + .AddImports("Shared.Engine") + .AddImports("Shared.Models") + .AddReferences(CSharpEval.ReferenceFromFile("Newtonsoft.Json.dll")) + .AddImports("Newtonsoft.Json") + .AddImports("Newtonsoft.Json.Linq"); + + return CSharpEval.Execute(nd.format, new CatalogNodeValue(value, host), options); + } + + return value?.Trim(); + } + #endregion + + #region nodeValue - JToken + public static object nodeValue(JToken node, SingleNodeSettings nd, string host) + { + if (node == null || nd == null) + return null; + + var current = node is JProperty property ? property.Value : node; + + JToken valueToken = null; + + if (!string.IsNullOrEmpty(nd.node)) + { + current = current.SelectToken(nd.node); + if (current == null) + return null; + } + + if (nd.attributes != null) + { + foreach (var attr in nd.attributes) + { + valueToken = current[attr]; + if (valueToken != null) + break; + } + } + + if (valueToken == null && !string.IsNullOrEmpty(nd.attribute)) + valueToken = current[nd.attribute]; + + if (valueToken == null) + return null; + + string value = valueToken switch + { + JValue jValue => jValue.Value?.ToString(), + JProperty jProp => jProp.Value?.ToString(), + _ => valueToken.ToString(Formatting.None) + }; + + if (string.IsNullOrEmpty(value)) + return null; + + if (nd.format != null) + { + var options = ScriptOptions.Default + .AddReferences(CSharpEval.ReferenceFromFile("Shared.dll")) + .AddImports("Shared") + .AddImports("Shared.Engine") + .AddImports("Shared.Models") + .AddReferences(CSharpEval.ReferenceFromFile("Newtonsoft.Json.dll")) + .AddImports("Newtonsoft.Json") + .AddImports("Newtonsoft.Json.Linq"); + + return CSharpEval.Execute(nd.format, new CatalogNodeValue(value, host), options); + } + + if (valueToken is JValue) + return value?.Trim(); + + return valueToken; + } + #endregion + + + #region setArgsValue + public static void setArgsValue(SingleNodeSettings arg, object val, JObject jo) + { + if (val != null) + { + if (arg.name_arg is "kp_rating" or "imdb_rating") + { + string rating = val?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(rating) && rating != "0" && rating != "0.0" && double.TryParse(rating, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) + { + rating = rating.Length > 3 ? rating.Substring(0, 3) : rating; + if (rating.Length == 1) + rating = $"{rating}.0"; + + jo[arg.name_arg] = JToken.FromObject(rating.Replace(",", ".")); + } + } + else if (arg.name_arg is "vote_average") + { + string value = val?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(value) && double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double _v) && _v > 0) + jo[arg.name_arg] = JToken.FromObject(_v); + } + else if (arg.name_arg is "runtime" or "PG") + { + string value = val?.ToString()?.Trim(); + if (!string.IsNullOrEmpty(value) && long.TryParse(value, out long _v) && _v > 0) + jo[arg.name_arg] = JToken.FromObject(_v); + } + else if (arg.name_arg is "genres" or "created_by" or "production_countries" or "production_companies" or "networks" or "spoken_languages") + { + if (val is string) + { + string arrayStr = val?.ToString(); + var array = new JArray(); + + if (!string.IsNullOrEmpty(arrayStr)) + { + foreach (string str in arrayStr.Split(",")) + { + if (string.IsNullOrWhiteSpace(str)) + continue; + + array.Add(new JObject() { ["name"] = clearText(str) }); + } + + jo[arg.name_arg] = array; + } + } + else if (IsStringList(val as JToken)) + { + var array = new JArray(); + foreach (var item in (JArray)val) + array.Add(new JObject() { ["name"] = clearText(item.ToString()) }); + + jo[arg.name_arg] = array; + } + else if (val is JToken token && token.Type == JTokenType.Array) + { + jo[arg.name_arg] = token; + } + } + else if (val is string && (arg.name_arg is "origin_country" or "languages")) + { + string arrayStr = val?.ToString(); + var array = new JArray(); + + if (!string.IsNullOrEmpty(arrayStr)) + { + foreach (string str in arrayStr.Split(",")) + { + if (!string.IsNullOrWhiteSpace(str)) + array.Add(str.Trim()); + } + + if (array.Count > 0) + jo[arg.name_arg] = array; + } + } + else + { + jo[arg.name_arg] = JToken.FromObject(val); + } + } + } + #endregion + + #region IsStringList + static bool IsStringList(JToken token) + { + if (token?.Type != JTokenType.Array) + return false; + + var array = token as JArray; + return array?.All(item => item.Type == JTokenType.String) == true; + } + #endregion + + #region clearText + public static string clearText(string text) + { + if (string.IsNullOrEmpty(text)) + return text; + + text = text.Replace(" ", ""); + text = Regex.Replace(text, "<[^>]+>", ""); + text = HttpUtility.HtmlDecode(text); + return text.Trim(); + } + #endregion + } +} diff --git a/DLNA/ApiController.cs b/DLNA/ApiController.cs new file mode 100644 index 0000000..bf71165 --- /dev/null +++ b/DLNA/ApiController.cs @@ -0,0 +1,1181 @@ +using DLNA.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using MongoDB.Driver; +using MonoTorrent; +using MonoTorrent.Client; +using NetVips; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Engine.JacRed; +using Shared.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using IO = System.IO; + +namespace DLNA.Controllers +{ + public class DLNAController : BaseController + { + #region DLNAController + static string dlna_path => AppInit.conf.dlna.path; + + static string defTrackers = "tr=http://retracker.local/announce&tr=http%3A%2F%2Fbt4.t-ru.org%2Fann%3Fmagnet&tr=http://retracker.mgts.by:80/announce&tr=http://tracker.city9x.com:2710/announce&tr=http://tracker.electro-torrent.pl:80/announce&tr=http://tracker.internetwarriors.net:1337/announce&tr=http://tracker2.itzmx.com:6961/announce&tr=udp://opentor.org:2710&tr=udp://public.popcorn-tracker.org:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=http://bt.svao-ix.ru/announce&tr=udp://explodie.org:6969/announce&tr=wss://tracker.btorrent.xyz&tr=wss://tracker.openwebtorrent.com"; + + static ClientEngine torrentEngine; + static DateTime lastBullderClientEngineCall = DateTime.MinValue; + + public static void Initialization() + { + Directory.CreateDirectory("cache/torrent"); + Directory.CreateDirectory($"{dlna_path}/"); + Directory.CreateDirectory($"{dlna_path}/thumbs/"); + Directory.CreateDirectory($"{dlna_path}/tmdb/"); + + ThreadPool.QueueUserWorkItem(async _ => + { + string trackers_best_ip = await Http.Get("https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt", timeoutSeconds: 20); + if (trackers_best_ip != null) + { + foreach (string line in trackers_best_ip.Split("\n")) + { + string tr = line.Replace("\n", "").Replace("\r", "").Trim(); + if (!string.IsNullOrWhiteSpace(tr)) + defTrackers += $"&tr={tr}"; + } + } + }); + + ThreadPool.QueueUserWorkItem(async _ => + { + while (true) + { + try + { + await Task.Delay(TimeSpan.FromMinutes(5)); + await removeClientEngine(); + } + catch { } + } + }); + + ThreadPool.QueueUserWorkItem(async _ => + { + while (true) + { + try + { + await Task.Delay(TimeSpan.FromMinutes(1)); + + if (torrentEngine == null) + continue; + + if (lastBullderClientEngineCall == DateTime.MinValue || DateTime.UtcNow - lastBullderClientEngineCall < TimeSpan.FromMinutes(10)) + continue; + + if (!HasActiveTorrentTasks()) + await removeClientEngine(); + } + catch { } + } + }); + + if (!Directory.Exists("cache/metadata")) + return; + + #region Resume download + var _files = Directory.GetFiles("cache/metadata", "*.torrent"); + if (_files.Length == 0) + return; + + bullderClientEngine(); + + foreach (string path in _files) + { + var t = Torrent.Load(path); + var manager = AppInit.conf.dlna.mode == "stream" ? torrentEngine.AddStreamingAsync(t, $"{dlna_path}/").Result : torrentEngine.AddAsync(t, $"{dlna_path}/").Result; + + //if (FastResume.TryLoad($"cache/fastresume/{t.InfoHash.ToHex()}.fresume", out FastResume resume)) + // manager.LoadFastResume(resume); + + int[] indexs = null; + + try + { + if (IO.File.Exists($"cache/metadata/{t.InfoHashes.V1.ToHex()}.json")) + indexs = JsonConvert.DeserializeObject(IO.File.ReadAllText($"cache/metadata/{t.InfoHashes.V1.ToHex()}.json")); + } + catch { } + + bool setPriority = false; + + manager.TorrentStateChanged += async (s, e) => + { + try + { + if (e != null && e.NewState == TorrentState.Seeding) + await e.TorrentManager.StopAsync(); + + if (e != null && (e.NewState == TorrentState.Metadata || e.NewState == TorrentState.Hashing || e.NewState == TorrentState.Downloading)) + { + if (!setPriority) + { + setPriority = true; + + if (indexs == null || indexs.Length == 0) + { + await manager.SetFilePriorityAsync(manager.Files[0], Priority.High); + } + else + { + for (int i = 0; i < manager.Files.Count; i++) + { + if (indexs.Contains(i)) + { + await manager.SetFilePriorityAsync(manager.Files[i], i == indexs[0] ? Priority.High : Priority.Normal); + } + else + { + await manager.SetFilePriorityAsync(manager.Files[i], Priority.DoNotDownload); + } + } + } + } + } + + if (e != null && (e.NewState == TorrentState.Stopped || e.NewState == TorrentState.Stopping)) + { + try + { + IO.File.Delete(path); + IO.File.Delete(path.Replace(".torrent", ".json")); + } + catch { } + + foreach (var f in e.TorrentManager.Files) + { + try + { + if (f.Priority == Priority.DoNotDownload && IO.File.Exists(f.FullPath)) + IO.File.Delete(f.FullPath); + } + catch { } + } + + await removeClientEngine(e.TorrentManager.InfoHashes.V1.ToHex().ToLower()); + } + } + catch { } + }; + } + #endregion + } + #endregion + + #region dlna.js + [HttpGet] + [AllowAnonymous] + [Route("dlna.js")] + [Route("dlna/js/{token}")] + public ActionResult Plugin(string token) + { + if (!AppInit.conf.dlna.enable) + return Content(string.Empty); + + var sb = new StringBuilder(FileCache.ReadAllText("plugins/dlna.js")); + + sb.Replace("{localhost}", host) + .Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + #region bullderClientEngine + static Task bullderClientEngine(int connectionTimeout = 10) + { + lastBullderClientEngineCall = DateTime.UtcNow; + + if (torrentEngine != null) + return Task.CompletedTask; + + EngineSettingsBuilder engineSettingsBuilder = new EngineSettingsBuilder() + { + MaximumHalfOpenConnections = 20, + ConnectionTimeout = TimeSpan.FromSeconds(connectionTimeout), + MaximumDownloadRate = AppInit.conf.dlna.downloadSpeed, + MaximumUploadRate = AppInit.conf.dlna.uploadSpeed, + MaximumDiskReadRate = AppInit.conf.dlna.maximumDiskReadRate, + MaximumDiskWriteRate = AppInit.conf.dlna.maximumDiskWriteRate + }; + + torrentEngine = new ClientEngine(engineSettingsBuilder.ToSettings()); + return torrentEngine.StartAllAsync(); + } + #endregion + + #region HasActiveTorrentTasks + static bool HasActiveTorrentTasks() + { + try + { + if (torrentEngine?.Torrents == null) + return false; + + foreach (var torrent in torrentEngine.Torrents) + { + if (torrent.State == TorrentState.Metadata || torrent.State == TorrentState.Downloading || torrent.State == TorrentState.Starting || torrent.State == TorrentState.Hashing) + return true; + } + } + catch { } + + return false; + } + #endregion + + #region removeClientEngine + async static Task removeClientEngine(string hash = null) + { + try + { + if (torrentEngine?.Torrents != null) + { + var tdl = new List(); + + foreach (var i in torrentEngine.Torrents) + { + if (hash != null) + { + if (i.InfoHashes.V1.ToHex().ToLower() == hash) + { + try + { + await i.StopAsync(TimeSpan.FromSeconds(20)); + } + catch { } + + tdl.Add(i); + } + } + else + { + if (i.State == TorrentState.Seeding || i.State == TorrentState.Stopped || i.State == TorrentState.Stopping) + { + try + { + await i.StopAsync(TimeSpan.FromSeconds(120)); + } + catch { } + + tdl.Add(i); + } + } + } + + if (tdl.Count > 0) + { + foreach (var item in tdl) + { + try + { + torrentEngine.Torrents.Remove(item); + } + catch { } + + try + { + await torrentEngine.RemoveAsync(item); + } + catch { } + } + + } + + if (torrentEngine.Torrents.Count == 0) + { + try + { + await torrentEngine.StopAllAsync(); + } + catch { } + + torrentEngine.Dispose(); + torrentEngine = null; + } + } + } + catch { } + } + #endregion + + #region getTorrent + async ValueTask<(string magnet, byte[] torrent)> getTorrent(string path) + { + if (!path.StartsWith("http")) + return (path, null); + + string memkey = $"DLNAController:getTorrent:{path}"; + if (!memoryCache.TryGetValue(memkey, out (string magnet, byte[] torrent) cache)) + { + var handler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }; + + handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + + using (var client = new System.Net.Http.HttpClient(handler)) + { + client.Timeout = TimeSpan.FromSeconds(10); + + using (var response = await client.GetAsync(path)) + { + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + using (var content = response.Content) + { + var t = await content.ReadAsByteArrayAsync(); + cache.magnet = BencodeTo.Magnet(t); + if (cache.magnet != null) + cache.torrent = t; + } + } + else if ((int)response.StatusCode is 301 or 302 or 307) + { + string location = response.Headers.Location?.ToString() ?? response.RequestMessage.RequestUri?.ToString(); + if (location != null && location.StartsWith("magnet:")) + cache.magnet = location; + } + } + } + + if (cache.magnet == null && cache.torrent == null) + return (null, null); + + memoryCache.Set(memkey, cache, DateTime.Now.AddMinutes(10)); + } + + return (cache.magnet, cache.torrent); + } + #endregion + + #region getTmdb + JObject getTmdb(string name) + { + try + { + string file = $"{dlna_path}/tmdb/{CrypTo.md5(name)}.json"; + if (IO.File.Exists(file)) + { + var tmdb = JsonConvert.DeserializeObject(IO.File.ReadAllText(file)); + tmdb.Remove("created_by"); + tmdb.Remove("networks"); + tmdb.Remove("production_companies"); + + return tmdb; + } + } + catch { } + + return null; + } + #endregion + + #region getEpisodes + JArray getEpisodes(JObject tmdb, string fileName) + { + try + { + if (tmdb == null || !tmdb.ContainsKey("number_of_seasons")) + return null; + + int season = getSeason(fileName); + + string file = $"{dlna_path}/tmdb/{tmdb.Value("id")}_season-{season}.json"; + if (IO.File.Exists(file)) + { + if (memoryCache.TryGetValue(file, out JArray episodes)) + return episodes; + + episodes = JsonConvert.DeserializeObject(IO.File.ReadAllText(file)).Value("episodes"); + + memoryCache.Set(file, episodes, DateTime.Now.AddSeconds(10)); + return episodes; + } + } + catch { } + + return null; + } + #endregion + + #region getEpisode + int getEpisode(string fileName) + { + if (int.TryParse(Regex.Match(fileName, "EP?([0-9]+)", RegexOptions.IgnoreCase).Groups[1].Value, out int _e) && _e > 0) + return _e; + + return 0; + } + #endregion + + #region getSeason + int getSeason(string fileName) + { + if (int.TryParse(Regex.Match(fileName, "S([0-9]+)", RegexOptions.IgnoreCase).Groups[1].Value, out int _s) && _s > 0) + return _s; + + return 0; + } + #endregion + + + #region Navigation + [HttpGet] + [Route("dlna")] + public ActionResult Index(string path) + { + if (!AppInit.conf.dlna.enable) + return Json(new { }); + + #region getImage + string getImage(string name) + { + string pathimage = $"thumbs/{name}.jpg"; + if (IO.File.Exists($"{dlna_path}/" + pathimage)) + return $"{host}/dlna/stream?path={HttpUtility.UrlEncode(pathimage)}"; + + return null; + } + #endregion + + #region getPreview + string getPreview(string name) + { + string pathimage = $"temp/{name}.mp4"; + if (IO.File.Exists($"{dlna_path}/" + pathimage)) + return $"{host}/dlna/stream?path={HttpUtility.UrlEncode(pathimage)}"; + + pathimage = $"temp/{name}.webm"; + if (IO.File.Exists($"{dlna_path}/" + pathimage)) + return $"{host}/dlna/stream?path={HttpUtility.UrlEncode(pathimage)}"; + + return null; + } + #endregion + + #region countFiles + int countFiles(string _path) + { + int count = 0; + + foreach (string file in Directory.GetFiles(_path)) + { + if (!Regex.IsMatch(Path.GetExtension(file), AppInit.conf.dlna.mediaPattern)) + continue; + + if (new FileInfo(file).Length > 0) + count++; + } + + return count; + } + #endregion + + var playlist = new List(); + + #region folders + foreach (string folder in Directory.GetDirectories($"{dlna_path}/" + path)) + { + if (folder.Contains("thumbs") || folder.Contains("tmdb") || folder.Contains("temp")) + continue; + + int length = countFiles(folder); + if (length > 0 || Directory.GetDirectories(folder).Length > 0) + { + playlist.Add(new DlnaModel() + { + type = "folder", + name = Path.GetFileName(folder), + uri = $"{host}/dlna?path={HttpUtility.UrlEncode(folder.Replace($"{dlna_path}/", ""))}", + img = getImage(CrypTo.md5(Path.GetFileName(folder))), + preview = getPreview(CrypTo.md5(Path.GetFileName(folder))), + path = folder.Replace($"{dlna_path}/", ""), + length = countFiles(folder), + creationTime = Directory.GetCreationTime(folder), + tmdb = getTmdb(Path.GetFileName(folder)) + }); + } + } + #endregion + + #region files + var filesTmdb = getTmdb(path); + var subtitles = Directory.GetFiles($"{dlna_path}/" + path, "*.srt"); + + foreach (string file in Directory.GetFiles($"{dlna_path}/" + path)) + { + if (!Regex.IsMatch(Path.GetExtension(file), AppInit.conf.dlna.mediaPattern)) + continue; + + string name = Path.GetFileName(file); + var fileinfo = new FileInfo(file); + if (fileinfo.Length == 0) + continue; + + JObject episodeTmdb = null; + + string img = getImage(CrypTo.md5(name)); + var episodes = getEpisodes(filesTmdb, name); + if (episodes != null) + { + int episode = getEpisode(name); + if (episode > 0) + { + episodeTmdb = episodes.FirstOrDefault(i => i.Value("episode_number") == episode)?.ToObject(); + episodeTmdb.Remove("crew"); + episodeTmdb.Remove("guest_stars"); + + if (episodeTmdb != null && episodeTmdb.ContainsKey("still_path")) + img = $"tmdb:/t/p/w400" + episodeTmdb.Value("still_path"); + } + } + + if (img == null) + img = getImage(CrypTo.md5($"{path}/{name}")); + + var dlnaModel = new DlnaModel() + { + type = "file", + name = name, + uri = $"{host}/dlna/stream?path={HttpUtility.UrlEncode(file.Replace($"{dlna_path}/", ""))}", + img = img, + preview = getPreview(CrypTo.md5(name)), + subtitles = new List(), + path = file.Replace($"{dlna_path}/", ""), + length = fileinfo.Length, + creationTime = fileinfo.CreationTime, + s = getSeason(name), + e = getEpisode(name), + tmdb = string.IsNullOrEmpty(path) ? getTmdb(name) : filesTmdb, + episode = episodeTmdb + }; + + #region subtitles + foreach (string subfile in subtitles) + { + if (subfile.Contains(Path.GetFileNameWithoutExtension(file))) + { + dlnaModel.subtitles.Add(new Subtitle() + { + label = "Sub #1", + url = $"{host}/dlna/stream?path={HttpUtility.UrlEncode($"{path}/{Path.GetFileName(subfile)}")}" + }); + } + } + #endregion + + playlist.Add(dlnaModel); + } + #endregion + + var jSettings = new JsonSerializerSettings() + { + DefaultValueHandling = DefaultValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore + }; + + if (string.IsNullOrWhiteSpace(path)) + { + #region torrentEngine + if (torrentEngine?.Torrents != null) + { + foreach (var t in torrentEngine.Torrents) + { + if (t.State == TorrentState.Metadata || t.State == TorrentState.Downloading || t.State == TorrentState.Starting) + { + if (t.Torrent?.Name == null || (!IO.File.Exists($"{dlna_path}/{t.Torrent.Name}") && !Directory.Exists($"{dlna_path}/{t.Torrent.Name}"))) + { + playlist.Add(new DlnaModel() + { + type = "file", + name = t.Torrent?.Name ?? t.InfoHashes.V1.ToHex(), + img = getImage(t.InfoHashes.V1.ToHex()) + }); + } + } + } + } + #endregion + + return ContentTo(JsonConvert.SerializeObject(playlist.OrderByDescending(i => i.creationTime), jSettings)); + } + + return ContentTo(JsonConvert.SerializeObject(playlist.OrderBy(i => + { + ulong.TryParse(Regex.Replace(i.name, "[^0-9]+", ""), out ulong ident); + return ident; + + }), jSettings)); + } + #endregion + + #region Stream + [HttpGet] + [Route("dlna/stream")] + public ActionResult Stream(string path) + { + if (!AppInit.conf.dlna.enable) + return Json(new { }); + + string contentType = "application/octet-stream"; + + if (path.EndsWith(".jpg")) + contentType = "image/jpeg"; + + return File(IO.File.OpenRead($"{dlna_path}/{path}"), contentType, true); + } + #endregion + + #region Delete + [HttpGet] + [Route("dlna/delete")] + public ActionResult Delete(string path) + { + if (!AppInit.conf.dlna.enable) + return Content(string.Empty); + + try + { + IO.File.Delete($"{dlna_path}/{path}"); + } + catch { } + + try + { + Directory.Delete($"{dlna_path}/{path}", true); + } + catch { } + + return Content(string.Empty); + } + #endregion + + + #region Managers + [HttpGet] + [Route("dlna/tracker/managers")] + public ActionResult Managers() + { + if (!AppInit.conf.dlna.enable || torrentEngine?.Torrents == null) + return Content("[]"); + + return Json(torrentEngine.Torrents.Select(i => new + { + InfoHash = i.InfoHashes.V1.ToHex(), + Name = i.Torrent?.Name ?? i.InfoHashes.V1.ToHex(), + //Engine = new + //{ + // i.Engine.ConnectionManager.HalfOpenConnections, + // i.Engine.ConnectionManager.OpenConnections, + // i.Engine.TotalDownloadSpeed, + // i.Engine.TotalUploadSpeed, + //}, + Files = i.Files?.Select(f => new + { + f.Path, + Priority = f.Priority.ToString(), + f.Length, + BytesDownloaded = f.BytesDownloaded() + }), + i.Monitor, + i.OpenConnections, + i.PartialProgress, + i.Progress, + i.Peers, + State = i.State.ToString(), + i.UploadingTo + })); + } + #endregion + + #region Show + [HttpGet] + [Route("dlna/tracker/show")] + async public Task Show(string path) + { + if (!AppInit.conf.dlna.enable) + return Json(new { error = "enable" }); + + try + { + var tparse = await getTorrent(path); + if (tparse.torrent != null) + return Json(Torrent.Load(tparse.torrent).Files.Select(i => new { i.Path })); + + if (tparse.magnet == null) + return Json(new { error = "magnet" }); + + string hash = Regex.Match(tparse.magnet, "btih:([a-z0-9]+)", RegexOptions.IgnoreCase).Groups[1].Value.ToLower(); + if (IO.File.Exists($"cache/torrent/{hash}")) + return Json(Torrent.Load(IO.File.ReadAllBytes($"cache/torrent/{hash}")).Files.Select(i => new { i.Path })); + + var s_cts = new CancellationTokenSource(); + s_cts.CancelAfter(1000 * 60 * 3); + + string magnet = tparse.magnet; + magnet += (magnet.Contains("?") ? "&" : "?") + defTrackers; + + #region trackers + //if (IO.File.Exists("cache/trackers.txt") && AppInit.conf.dlna.addTrackersToMagnet) + //{ + // foreach (string line in IO.File.ReadLines("cache/trackers.txt")) + // { + // if (string.IsNullOrWhiteSpace(line)) + // continue; + + // if (line.StartsWith("http") || line.StartsWith("udp:")) + // { + // string host = line.Replace("\r", "").Replace("\n", "").Replace("\t", "").Trim(); + // string tr = HttpUtility.UrlEncode(host); + + // if (!magnet.Contains(tr)) + // magnet += $"&tr={tr}"; + // } + // } + //} + #endregion + + await bullderClientEngine(); + + if (torrentEngine.Torrents.FirstOrDefault(i => i.InfoHashes.V1.ToHex().ToLower() == hash) is TorrentManager manager) + { + if (manager.Files != null) + return Json(manager.Files.Select(i => (ITorrentFile)i).Select(i => new { i.Path })); + + await manager.WaitForMetadataAsync(s_cts.Token); + var files = manager.Files.Select(i => (ITorrentFile)i); + return Json(files.Select(i => new { i.Path })); + } + + var data = await torrentEngine.DownloadMetadataAsync(MagnetLink.Parse(magnet), s_cts.Token); + if (data.IsEmpty) + return Json(new { error = "DownloadMetadata" }); + + IO.File.WriteAllBytes($"cache/torrent/{hash}", data.Span); + + return Json(Torrent.Load(data.Span).Files.Select(i => new { i.Path })); + } + catch (Exception ex) + { + return Json(new { error = ex.ToString() }); + } + } + #endregion + + #region Download + [HttpGet] + [Route("dlna/tracker/download")] + async public Task Download(string path, int[] indexs, string thumb, long id, bool serial, int lastCount = -1) + { + if (!AppInit.conf.dlna.enable) + return Json(new { error = "enable" }); + + try + { + var tparse = await getTorrent(path); + if (tparse.magnet == null) + return Json(new { error = "magnet" }); + + // cache metadata + string hash = Regex.Match(tparse.magnet, "btih:([a-z0-9]+)", RegexOptions.IgnoreCase).Groups[1].Value.ToLower(); + if (IO.File.Exists($"cache/torrent/{hash}") && !IO.File.Exists($"cache/metadata/{hash.ToUpper()}.torrent")) + IO.File.Copy($"cache/torrent/{hash}", $"cache/metadata/{hash.ToUpper()}.torrent"); + + var magnetLink = MagnetLink.Parse(tparse.magnet + (tparse.magnet.Contains("?") ? "&" : "?") + defTrackers); + + await bullderClientEngine(); + TorrentManager manager = torrentEngine.Torrents.FirstOrDefault(i => i.InfoHashes.V1.ToHex() == magnetLink.InfoHashes.V1.ToHex()); + + ThreadPool.QueueUserWorkItem(async _ => + { + try + { + #region Download thumb + if (thumb != null) + { + try + { + #region IsValidImg + bool IsValidImg(byte[] _img) + { + if (AppInit.conf.imagelibrary == "NetVips") + return IsValidImgage(_img, path); + + return true; + } + #endregion + + string uri = Regex.Replace(thumb, "^https?://[^/]+/", ""); + + var array = await Http.Download($"https://image.tmdb.org/{uri}", timeoutSeconds: 10); + if (array == null || !IsValidImg(array)) + array = await Http.Download($"https://imagetmdb.{AppInit.conf.cub.mirror}/{uri}", timeoutSeconds: 10); + + if (array != null && IsValidImg(array)) + { + Directory.CreateDirectory($"{dlna_path}/thumbs"); + IO.File.WriteAllBytes($"{dlna_path}/thumbs/{magnetLink.InfoHashes.V1.ToHex()}.jpg", array); + } + } + catch { } + } + #endregion + + if (manager == null) + { + dynamic tlink = tparse.torrent != null ? Torrent.Load(tparse.torrent) : magnetLink; + manager = AppInit.conf.dlna.mode == "stream" ? await torrentEngine.AddStreamingAsync(tlink, $"{dlna_path}/") : await torrentEngine.AddAsync(tlink, $"{dlna_path}/"); + + #region AddTrackerAsync + if (IO.File.Exists("cache/trackers.txt") && AppInit.conf.dlna.addTrackersToMagnet) + { + foreach (string line in IO.File.ReadLines("cache/trackers.txt").OrderBy(x => Random.Shared.Next())) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + string host = line.Replace("\r", "").Replace("\n", "").Replace("\t", "").Trim(); + + if (host.StartsWith("http") || host.StartsWith("udp:")) + { + try + { + await manager.TrackerManager.AddTrackerAsync(new Uri(host)); + } + catch { } + } + } + } + #endregion + } + + await manager.StartAsync(); + await manager.WaitForMetadataAsync(); + + #region thumb + if (IO.File.Exists($"{dlna_path}/thumbs/{magnetLink.InfoHashes.V1.ToHex()}.jpg")) + { + try + { + IO.File.Copy($"{dlna_path}/thumbs/{magnetLink.InfoHashes.V1.ToHex()}.jpg", $"{dlna_path}/thumbs/{CrypTo.md5(manager.Torrent.Name)}.jpg", true); + } + catch { } + } + #endregion + + #region TorrentStateChanged + bool dispose = false; + + manager.TorrentStateChanged += async (s, e) => + { + try + { + if (!dispose && e != null && (e.NewState == TorrentState.Seeding || e.NewState == TorrentState.Stopped || e.NewState == TorrentState.Stopping)) + { + dispose = true; + + try + { + IO.File.Delete($"cache/metadata/{e.TorrentManager.InfoHashes.V1.ToHex()}.torrent"); + IO.File.Delete($"cache/metadata/{e.TorrentManager.InfoHashes.V1.ToHex()}.json"); + } + catch { } + + foreach (var f in e.TorrentManager.Files) + { + try + { + if (f.Priority == Priority.DoNotDownload && IO.File.Exists(f.FullPath)) + { + if (f.Length == 0 || f.BytesDownloaded() == 0) + { + IO.File.Delete(f.FullPath); + } + else + { + double percentageDownloaded = (double)f.BytesDownloaded() / f.Length; + + if (percentageDownloaded < 0.9) + IO.File.Delete(f.FullPath); + } + } + } + catch { } + } + + await removeClientEngine(e.TorrentManager.InfoHashes.V1.ToHex().ToLower()); + } + } + catch { } + }; + #endregion + + #region lastCount + if (lastCount > 0 && manager.Files.Count >= lastCount) + { + var _indexs = new List(); + for (int i = manager.Files.Count-1; i >= 0; i--) + { + if (_indexs.Count == lastCount) + break; + + if (!Regex.IsMatch(Path.GetExtension(manager.Files[i].Path), AppInit.conf.dlna.mediaPattern)) + continue; + + _indexs.Add(i); + } + + indexs = _indexs.ToArray(); + } + #endregion + + #region indexs + if (indexs == null || indexs.Length == 0) + { + foreach (var file in manager.Files) + { + if (file.Priority != Priority.Normal) + await manager.SetFilePriorityAsync(file, Priority.Normal); + } + } + else + { + Directory.CreateDirectory("cache/metadata/"); + IO.File.WriteAllText($"cache/metadata/{manager.InfoHashes.V1.ToHex()}.json", JsonConvert.SerializeObject(indexs)); + + for (int i = 0; i < manager.Files.Count; i++) + { + if (indexs.Contains(i)) + { + if (manager.Files[i].Priority != Priority.Normal) + await manager.SetFilePriorityAsync(manager.Files[i], Priority.Normal); + } + else + { + if (manager.Files[i].Priority != Priority.DoNotDownload) + await manager.SetFilePriorityAsync(manager.Files[i], Priority.DoNotDownload); + } + } + } + #endregion + + #region tmdb + string cat = serial ? "tv" : "movie"; + var header = HeadersModel.Init(("localrequest", AppInit.rootPasswd)); + string json = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/{cat}/{id}?api_key={AppInit.conf.tmdb.api_key}&language=ru", timeoutSeconds: 20, headers: header); + + if (string.IsNullOrEmpty(json)) + json = await Http.Get($"https://apitmdb.{AppInit.conf.cub.mirror}/3/{cat}/{id}?api_key={AppInit.conf.tmdb.api_key}&language=ru", timeoutSeconds: 20); + + if (!string.IsNullOrEmpty(json)) + { + IO.File.WriteAllText($"{dlna_path}/tmdb/{CrypTo.md5(manager.Torrent.Name)}.json", json); + + if (serial) + { + if (int.TryParse(Regex.Match(json, "\"number_of_seasons\":([0-9 ]+)").Groups[1].Value.Trim(), out int number_of_seasons) && number_of_seasons > 0) + { + async ValueTask write(int s) + { + string seasons = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/3/{cat}/{id}/season/{s}?api_key={AppInit.conf.tmdb.api_key}&language=ru", timeoutSeconds: 20, headers: header); + + if (string.IsNullOrEmpty(seasons)) + seasons = await Http.Get($"https://apitmdb.{AppInit.conf.cub.mirror}/3/{cat}/{id}/season/{s}?api_key={AppInit.conf.tmdb.api_key}&language=ru", timeoutSeconds: 20); + + if (!string.IsNullOrEmpty(seasons)) + IO.File.WriteAllText($"{dlna_path}/tmdb/{id}_season-{s}.json", json); + } + + if (number_of_seasons == 1) + await write(number_of_seasons); + else + { + foreach (var f in manager.Files) + { + int s = getSeason(Path.GetFileName(f.Path)); + if (s > 0) + await write(s); + } + } + } + } + } + #endregion + } + catch { } + }); + } + catch (Exception ex) + { + return Json(new { error = ex.ToString() }); + } + + return Json(new { status = true }); + } + #endregion + + + #region Delete + [HttpGet] + [Route("dlna/tracker/stop")] + [Route("dlna/tracker/delete")] + async public Task TorrentDelete(string infohash) + { + if (!AppInit.conf.dlna.enable || torrentEngine == null) + return Json(new { }); + + var manager = torrentEngine.Torrents.FirstOrDefault(i => i.InfoHashes.V1.ToHex() == infohash); + if (manager != null) + { + try + { + IO.File.Delete($"cache/metadata/{manager.InfoHashes.V1.ToHex()}.torrent"); + IO.File.Delete($"cache/metadata/{manager.InfoHashes.V1.ToHex()}.json"); + } + catch { } + + try + { + await manager.StopAsync(); + } + catch { } + + await removeClientEngine(manager.InfoHashes.V1.ToHex().ToLower()); + } + + return Json(new { status = true }); + } + #endregion + + #region Pause + [HttpGet] + [Route("dlna/tracker/pause")] + async public Task TorrentPause(string infohash) + { + if (!AppInit.conf.dlna.enable || torrentEngine == null) + return Json(new { }); + + var manager = torrentEngine.Torrents.FirstOrDefault(i => i.InfoHashes.V1.ToHex() == infohash); + if (manager != null) + await manager.PauseAsync(); + + return Json(new { status = true }); + } + #endregion + + #region Start + [HttpGet] + [Route("dlna/tracker/start")] + async public Task TorrentStart(string infohash) + { + if (!AppInit.conf.dlna.enable || torrentEngine == null) + return Json(new { }); + + var manager = torrentEngine.Torrents.FirstOrDefault(i => i.InfoHashes.V1.ToHex() == infohash); + if (manager != null) + await manager.StartAsync(); + + return Json(new { status = true }); + } + #endregion + + #region ChangeFilePriority + [HttpGet] + [Route("dlna/tracker/changefilepriority")] + async public Task ChangeFilePriority(string infohash, int[] indexs) + { + if (!AppInit.conf.dlna.enable || torrentEngine == null) + return Json(new { }); + + var manager = torrentEngine.Torrents.FirstOrDefault(i => i.InfoHashes.V1.ToHex() == infohash); + if (manager != null) + { + if (indexs == null || indexs.Length == 0) + { + foreach (var file in manager.Files) + { + if (file.Priority != Priority.Normal) + await manager.SetFilePriorityAsync(file, Priority.Normal); + } + + if (IO.File.Exists($"cache/metadata/{manager.InfoHashes.V1.ToHex()}.json")) + IO.File.Delete($"cache/metadata/{manager.InfoHashes.V1.ToHex()}.json"); + } + else + { + Directory.CreateDirectory("cache/metadata/"); + IO.File.WriteAllText($"cache/metadata/{manager.InfoHashes.V1.ToHex()}.json", JsonConvert.SerializeObject(indexs)); + + for (int i = 0; i < manager.Files.Count; i++) + { + if (indexs.Contains(i)) + { + if (manager.Files[i].Priority != Priority.Normal) + await manager.SetFilePriorityAsync(manager.Files[i], Priority.Normal); + } + else + { + if (manager.Files[i].Priority != Priority.DoNotDownload) + await manager.SetFilePriorityAsync(manager.Files[i], Priority.DoNotDownload); + } + } + } + } + + return Json(new { status = true }); + } + #endregion + + + + #region IsValidImgage + static bool IsValidImgage(byte[] _img, string path) + { + if (_img == null) + return false; + + using (var image = Image.NewFromBuffer(_img)) + { + try + { + if (!path.Contains(".svg")) + { + // тестируем jpg/png на целостность + byte[] temp = image.JpegsaveBuffer(); + if (temp == null || temp.Length == 0) + return false; + } + + return true; + } + catch + { + return false; + } + } + } + #endregion + } +} \ No newline at end of file diff --git a/DLNA/DLNA.csproj b/DLNA/DLNA.csproj new file mode 100644 index 0000000..a644285 --- /dev/null +++ b/DLNA/DLNA.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + library + true + + + + + + + diff --git a/DLNA/ModInit.cs b/DLNA/ModInit.cs new file mode 100644 index 0000000..b794788 --- /dev/null +++ b/DLNA/ModInit.cs @@ -0,0 +1,188 @@ +using DLNA.Controllers; +using Shared; +using Shared.Engine; +using System; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace DLNA +{ + public class ModInit + { + public static void loaded() + { + DLNAController.Initialization(); + + var init = AppInit.conf.dlna; + var cover = init.cover; + Directory.CreateDirectory($"{init.path}/temp/"); + + ThreadPool.QueueUserWorkItem(async _ => + { + bool? ffmpegInit = null; + + while (true) + { + if (cover.timeout == -666) + await Task.Delay(TimeSpan.FromSeconds(5)); + else + await Task.Delay(TimeSpan.FromMinutes(cover.timeout > 0 ? cover.timeout : 1)); + + if (!init.enable || !cover.enable) + continue; + + if (ffmpegInit == null) + { + ffmpegInit = await FFmpeg.InitializationAsync(); + if (ffmpegInit == false) + break; + } + + try + { + #region path files + foreach (string file in Directory.GetFiles(init.path)) + { + if (!Regex.IsMatch(Path.GetExtension(file), cover.extension)) + continue; + + string name = Path.GetFileName(file); + var fileinfo = new FileInfo(file); + if (fileinfo.Length == 0) + continue; + + var time = fileinfo.CreationTime > fileinfo.LastWriteTime ? fileinfo.CreationTime : fileinfo.LastWriteTime; + if (time.AddMinutes(cover.skipModificationTime) > DateTime.Now) + { + log("skip time: " + file); + continue; + } + + string thumb = Path.Combine(init.path, "thumbs", $"{CrypTo.md5(name)}.jpg"); + if (File.Exists(thumb)) + { + log("thumb ok: " + file); + continue; + } + + string lockfile = Path.Combine(init.path, "temp", $"{CrypTo.md5(name)}-ffmpeg.lock"); + if (File.Exists(lockfile)) + { + log("lock: " + file); + continue; + } + + File.Create(lockfile); + + string coverComand = cover.coverComand.Replace("{file}", file).Replace("{thumb}", thumb); + log("\ncoverComand: " + coverComand); + var ffmpegLog = await FFmpeg.RunAsync(coverComand, priorityClass: cover.priorityClass); + + log(ffmpegLog.outputData); + log(ffmpegLog.errorData); + + if (cover.preview) + { + string preview = Path.Combine(init.path, "temp", $"{CrypTo.md5(name)}.mp4"); + string previewComand = cover.previewComand.Replace("{file}", file).Replace("{preview}", preview); + + log("\npreviewComand: " + previewComand); + ffmpegLog = await FFmpeg.RunAsync(previewComand, priorityClass: cover.priorityClass); + + log(ffmpegLog.outputData); + log(ffmpegLog.errorData); + } + } + #endregion + + #region path directories + foreach (string folder in Directory.GetDirectories(init.path)) + { + if (folder.Contains("thumbs") || folder.Contains("tmdb") || folder.Contains("temp")) + continue; + + string folder_name = Path.GetFileName(folder); + string folder_thumb = Path.Combine(init.path, "thumbs", $"{CrypTo.md5(folder_name)}.jpg"); + if (File.Exists(folder_thumb)) + { + log("thumb ok: " + folder); + continue; + } + + var files = Directory.GetFiles(folder); + if (files.Length == 0) + continue; + + var folderinfo = new DirectoryInfo(folder); + var time = folderinfo.CreationTime > folderinfo.LastWriteTime ? folderinfo.CreationTime : folderinfo.LastWriteTime; + if (time.AddMinutes(cover.skipModificationTime) > DateTime.Now) + { + log("skip time: " + folder); + continue; + } + + string lockfile = Path.Combine(init.path, "temp", $"{CrypTo.md5(folder_name)}-ffmpeg.lock"); + if (File.Exists(lockfile)) + { + log("lock: " + folder); + continue; + } + + File.Create(lockfile); + + #region постер с превью на папку + { + string coverComand = cover.coverComand.Replace("{file}", files[0]).Replace("{thumb}", folder_thumb); + log("\ncoverComand: " + coverComand); + var ffmpegLog = await FFmpeg.RunAsync(coverComand, priorityClass: cover.priorityClass); + + log(ffmpegLog.outputData); + log(ffmpegLog.errorData); + + if (cover.preview) + { + string preview = Path.Combine(init.path, "temp", $"{CrypTo.md5(folder_name)}.mp4"); + string previewComand = cover.previewComand.Replace("{file}", files[0]).Replace("{preview}", preview); + + log("\npreviewComand: " + previewComand); + ffmpegLog = await FFmpeg.RunAsync(previewComand, priorityClass: cover.priorityClass); + + log(ffmpegLog.outputData); + log(ffmpegLog.errorData); + } + } + #endregion + + #region постеры на файлы внутри папки + foreach (string file in files) + { + string name = $"{Path.GetFileName(folder)}/{Path.GetFileName(file)}"; + string thumb = Path.Combine(init.path, "thumbs", $"{CrypTo.md5(name)}.jpg"); + + string coverComand = cover.coverComand.Replace("{file}", file).Replace("{thumb}", thumb); + log("\ncoverComand: " + coverComand); + var ffmpegLog = await FFmpeg.RunAsync(coverComand, priorityClass: cover.priorityClass); + + log(ffmpegLog.outputData); + log(ffmpegLog.errorData); + } + #endregion + } + #endregion + } + catch { } + } + + }); + } + + + public static void log(string value) + { + if (AppInit.conf.dlna.cover.consoleLog && !string.IsNullOrEmpty(value)) + Console.WriteLine("\nFFmpeg: " + value); + } + } +} diff --git a/DLNA/Models/DlnaModel.cs b/DLNA/Models/DlnaModel.cs new file mode 100644 index 0000000..dfbb9cd --- /dev/null +++ b/DLNA/Models/DlnaModel.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace DLNA.Models +{ + public class DlnaModel + { + public string name { get; set; } + + public string uri { get; set; } + + public string img { get; set; } + + public string preview { get; set; } + + public List subtitles { get; set; } + + public string path { get; set; } + + public string type { get; set; } + + public long length { get; set; } + + public DateTime creationTime { get; set; } + + public int s { get; set; } + + public int e { get; set; } + + public JObject tmdb { get; set; } + + public JObject episode { get; set; } + } +} diff --git a/DLNA/Models/Subtitle.cs b/DLNA/Models/Subtitle.cs new file mode 100644 index 0000000..14f3359 --- /dev/null +++ b/DLNA/Models/Subtitle.cs @@ -0,0 +1,9 @@ +namespace DLNA.Models +{ + public class Subtitle + { + public string label { get; set; } + + public string url { get; set; } + } +} diff --git a/JacRed/ApiController.cs b/JacRed/ApiController.cs new file mode 100644 index 0000000..0641e3d --- /dev/null +++ b/JacRed/ApiController.cs @@ -0,0 +1,375 @@ +using Jackett; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared.Engine.Utilities; + +namespace JacRed.Controllers +{ + public class ApiController : JacBaseController + { + #region Conf + [HttpGet] + [Route("api/v1.0/conf")] + public JsonResult JacConf(string apikey) + { + return Json(new + { + apikey = string.IsNullOrWhiteSpace(AppInit.conf.apikey) || apikey == AppInit.conf.apikey + }); + } + #endregion + + #region Indexers + [HttpGet] + [Route("/api/v2.0/indexers/{status}/results")] + async public Task Indexers(string apikey, string query, string title, string title_original, int year, Dictionary category, int is_serial = -1) + { + if (string.IsNullOrEmpty(ModInit.conf.typesearch)) + return Content("typesearch == null"); + + #region Запрос с NUM + bool rqnum = !HttpContext.Request.QueryString.Value.Contains("&is_serial=") && HttpContext.Request.Headers.UserAgent.ToString() == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"; + + if (rqnum && query != null) + { + var mNum = Regex.Match(query, "^([^a-z-A-Z]+) ([^а-я-А-Я]+) ([0-9]{4})$"); + + if (mNum.Success) + { + if (Regex.IsMatch(mNum.Groups[2].Value, "[a-zA-Z0-9]{2}")) + { + var g = mNum.Groups; + title = g[1].Value; + title_original = g[2].Value; + year = int.Parse(g[3].Value); + } + } + else + { + if (Regex.IsMatch(query, "^([^a-z-A-Z]+) ((19|20)[0-9]{2})$")) + return Content(JsonConvertPool.SerializeObject(new { Results = new List(), jacred = ModInit.conf.typesearch == "red" }), "application/json; charset=utf-8"); + + mNum = Regex.Match(query, "^([^a-z-A-Z]+) ([^а-я-А-Я]+)$"); + + if (mNum.Success) + { + if (Regex.IsMatch(mNum.Groups[2].Value, "[a-zA-Z0-9]{2}")) + { + var g = mNum.Groups; + title = g[1].Value; + title_original = g[2].Value; + } + } + } + } + #endregion + + if (!HttpContext.Request.QueryString.Value.ToLower().Contains("&category[]=")) + category = null; + + IEnumerable torrents = null; + + if (ModInit.conf.typesearch == "red") + { + #region red + string memoryKey = $"{ModInit.conf.typesearch}:{query}:{rqnum}:{title}:{title_original}:{year}:{is_serial}"; + if (!hybridCache.TryGetValue(memoryKey, out List _redCache, inmemory: false)) + { + var res = RedApi.Indexers(rqnum, apikey, query, title, title_original, year, is_serial, category); + + _redCache = res.torrents.ToList(); + + if (res.setcache && !red.evercache.enable) + hybridCache.Set(memoryKey, _redCache, DateTime.Now.AddMinutes(5), inmemory: false); + } + + if (ModInit.conf.merge == "jackett") + { + torrents = mergeTorrents + ( + _redCache, + await JackettApi.Indexers(host, query, title, title_original, year, is_serial, category) + ); + } + else + { + torrents = _redCache; + } + #endregion + } + else if (ModInit.conf.typesearch == "webapi") + { + #region webapi + if (ModInit.conf.merge == "jackett") + { + var t1 = WebApi.Indexers(query, title, title_original, year, is_serial, category); + var t2 = JackettApi.Indexers(host, query, title, title_original, year, is_serial, category); + + await Task.WhenAll(t1, t2); + + torrents = mergeTorrents(t1.Result, t2.Result); + } + else + { + torrents = await WebApi.Indexers(query, title, title_original, year, is_serial, category); + } + #endregion + } + else if (ModInit.conf.typesearch == "jackett") + { + torrents = await JackettApi.Indexers(host, query, title, title_original, year, is_serial, category); + } + + return Content(JsonConvert.SerializeObject(new + { + Results = torrents.OrderByDescending(i => i.createTime).Take(2_000).Select(i => new + { + Tracker = i.trackerName, + Details = i.url != null && i.url.StartsWith("http") ? i.url : null, + Title = i.title, + Size = (long)(0 >= i.size ? getSizeInfo(i.sizeName) : i.size), + PublishDate = i.createTime, + Category = getCategoryIds(i, out string categoryDesc), + CategoryDesc = categoryDesc, + Seeders = i.sid, + Peers = i.pir, + MagnetUri = i.magnet, + Link = i.parselink != null ? $"{i.parselink}&apikey={apikey}" : null, + Info = ModInit.conf.typesearch != "red" || rqnum ? null : new + { + i.name, + i.originalname, + i.relased, + i.quality, + i.videotype, + i.sizeName, + i.voices, + seasons = i.seasons != null && i.seasons.Count > 0 ? i.seasons : null, + i.types + }, + languages = !rqnum && i.languages != null && i.languages.Count > 0 ? i.languages : null, + ffprobe = rqnum ? null : i.ffprobe + }), + jacred = ModInit.conf.typesearch == "red" + + }, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), "application/json; charset=utf-8"); + } + #endregion + + #region Api + [HttpGet] + [Route("/api/v1.0/torrents")] + async public Task Api(string apikey, string search, string altname, bool exact, string type, string sort, string tracker, string voice, string videotype, long relased, long quality, long season) + { + if (string.IsNullOrEmpty(ModInit.conf.typesearch)) + return Content("typesearch == null"); + + #region search kp/imdb + if (!string.IsNullOrWhiteSpace(search) && Regex.IsMatch(search.Trim(), "^(tt|kp)[0-9]+$")) + { + string memkey = $"api/v1.0/torrents:{search}"; + if (!hybridCache.TryGetValue(memkey, out (string original_name, string name) cache, inmemory: false)) + { + search = search.Trim(); + string uri = $"&imdb={search}"; + if (search.StartsWith("kp")) + uri = $"&kp={search.Remove(0, 2)}"; + + var root = await Http.Get("https://api.alloha.tv/?token=04941a9a3ca3ac16e2b4327347bbc1" + uri, timeoutSeconds: 10); + cache.original_name = root?.Value("data")?.Value("original_name"); + cache.name = root?.Value("data")?.Value("name"); + + hybridCache.Set(memkey, cache, DateTime.Now.AddDays(1), inmemory: false); + } + + if (!string.IsNullOrWhiteSpace(cache.name) && !string.IsNullOrWhiteSpace(cache.original_name)) + { + search = cache.original_name; + altname = cache.name; + } + else + { + search = cache.original_name ?? cache.name; + } + } + #endregion + + IEnumerable torrents = null; + + if (ModInit.conf.typesearch == "red") + { + #region red + torrents = RedApi.Api(search, altname, exact, type, sort, tracker, voice, videotype, relased, quality, season); + + if (ModInit.conf.merge == "jackett") + { + torrents = mergeTorrents + ( + torrents, + await JackettApi.Api(host, search) + ); + } + #endregion + } + else if (ModInit.conf.typesearch == "webapi") + { + #region webapi + if (ModInit.conf.merge == "jackett") + { + var t1 = WebApi.Api(search); + var t2 = JackettApi.Api(host, search); + + await Task.WhenAll(t1, t2); + + torrents = mergeTorrents(t1.Result, t2.Result); + } + else + { + torrents = await WebApi.Api(search); + } + #endregion + } + else if (ModInit.conf.typesearch == "jackett") + { + torrents = await JackettApi.Api(host, search); + } + + return Content(JsonConvert.SerializeObject(torrents.Take(2_000).Select(i => new + { + tracker = i.trackerName, + url = i.url != null && i.url.StartsWith("http") ? i.url : null, + i.title, + size = 0 > i.size ? getSizeInfo(i.sizeName) : i.size, + i.sizeName, + i.createTime, + i.sid, + i.pir, + magnet = i.magnet ?? $"{i.parselink}&apikey={apikey}", + i.name, + i.originalname, + i.relased, + i.videotype, + i.quality, + i.voices, + i.seasons, + i.types + + }), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), "application/json; charset=utf-8"); + } + #endregion + + + #region getSizeInfo + long getSizeInfo(string sizeName) + { + if (string.IsNullOrWhiteSpace(sizeName)) + return 0; + + try + { + double size = 0.1; + var gsize = Regex.Match(sizeName, "([0-9\\.,]+) (Mb|МБ|GB|ГБ|TB|ТБ)", RegexOptions.IgnoreCase).Groups; + if (!string.IsNullOrWhiteSpace(gsize[2].Value)) + { + if (double.TryParse(gsize[1].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out size) && size != 0) + { + if (gsize[2].Value.ToLower() is "gb" or "гб") + size *= 1024; + + if (gsize[2].Value.ToLower() is "tb" or "тб") + size *= 1048576; + + return (long)(size * 1048576); + } + } + } + catch { } + + return 0; + } + #endregion + + #region getCategoryIds + HashSet getCategoryIds(TorrentDetails t, out string categoryDesc) + { + categoryDesc = null; + HashSet categoryIds = new HashSet(); + + if (t.types == null) + return categoryIds; + + foreach (string type in t.types) + { + switch (type) + { + case "movie": + categoryDesc = "Movies"; + categoryIds.Add(2000); + break; + + case "serial": + categoryDesc = "TV"; + categoryIds.Add(5000); + break; + + case "documovie": + case "docuserial": + categoryDesc = "TV/Documentary"; + categoryIds.Add(5080); + break; + + case "tvshow": + categoryDesc = "TV/Foreign"; + categoryIds.Add(5020); + categoryIds.Add(2010); + break; + + case "anime": + categoryDesc = "TV/Anime"; + categoryIds.Add(5070); + break; + } + } + + return categoryIds; + } + #endregion + + #region mergeTorrents + static IEnumerable mergeTorrents(IEnumerable red, IEnumerable jac) + { + if (red == null && jac == null) + return new List(); + + if (red == null || !red.Any()) + return jac; + + if (jac == null || !jac.Any()) + return red; + + var torrents = new Dictionary(); + + foreach (var i in red.Concat(jac)) + { + if (string.IsNullOrEmpty(i.url) || !i.url.StartsWith("http")) + continue; + + void add(string url) { torrents.TryAdd(Regex.Replace(url, "^https?://[^/]+/", ""), (TorrentDetails)i.Clone()); } + + if (i.urls != null && i.urls.Count > 0) + { + foreach (string u in i.urls) + add(u); + } + else + { + add(i.url); + } + } + + return torrents.Values; + } + #endregion + } +} diff --git a/JacRed/Controllers/AniLibriaController.cs b/JacRed/Controllers/AniLibriaController.cs new file mode 100644 index 0000000..38678f5 --- /dev/null +++ b/JacRed/Controllers/AniLibriaController.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Mvc; +using JacRed.Models.AniLibria; + +namespace JacRed.Controllers +{ + [Route("anilibria/[action]")] + public class AniLibriaController : JacBaseController + { + #region parseMagnet + async public Task parseMagnet(string url, string code) + { + if (!jackett.Anilibria.enable || jackett.Anilibria.showdown) + return Content("disable"); + + var proxyManager = new ProxyManager("anilibria", jackett.Anilibria); + + byte[] _t = await Http.Download($"{jackett.Anilibria.host}/{url}", referer: $"{jackett.Anilibria.host}/release/{code}.html", proxy: proxyManager.Get()); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + proxyManager.Refresh(); + return Content("error"); + } + #endregion + + #region parsePage + async public static Task search(string host, ConcurrentBag torrents, string query) + { + if (!jackett.Anilibria.enable) + return false; + + var proxyManager = new ProxyManager("anilibria", jackett.Anilibria); + + var roots = await Http.Get>("https://api.anilibria.tv/v2/searchTitles?search=" + HttpUtility.UrlEncode(query), timeoutSeconds: jackett.timeoutSeconds, proxy: proxyManager.Get(), IgnoreDeserializeObject: true); + if (roots == null || roots.Count == 0) + { + consoleErrorLog("anilibria"); + proxyManager.Refresh(); + return false; + } + + foreach (var root in roots) + { + DateTime createTime = new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(root.last_change > root.updated ? root.last_change : root.updated); + + foreach (var torrent in root.torrents.list) + { + if (string.IsNullOrWhiteSpace(root.code) || 480 >= torrent.quality.resolution && string.IsNullOrWhiteSpace(torrent.quality.encoder) && string.IsNullOrWhiteSpace(torrent.url)) + continue; + + torrents.Add(new TorrentDetails() + { + trackerName = "anilibria.tv", + types = new string[] { "anime" }, + url = $"{jackett.Anilibria.host}/release/{root.code}.html", + title = $"{root.names.ru} / {root.names.en} {root.season.year} (s{root.season.code}, e{torrent.series.@string}) [{torrent.quality.@string}]", + sid = torrent.seeders, + pir = torrent.leechers, + createTime = createTime, + parselink = $"{host}/anilibria/parsemagnet?url={HttpUtility.UrlEncode(torrent.url)}&code={root.code}", + sizeName = tParse.BytesToString(torrent.total_size), + name = root.names.ru, + originalname = root.names.en, + relased = root.season.year + }); + } + } + + + return true; + } + #endregion + } +} diff --git a/JacRed/Controllers/AnifilmController.cs b/JacRed/Controllers/AnifilmController.cs new file mode 100644 index 0000000..baf6bf4 --- /dev/null +++ b/JacRed/Controllers/AnifilmController.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Mvc; + +namespace JacRed.Controllers +{ + [Route("anifilm/[action]")] + public class AnifilmController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query) + { + if (!jackett.Anifilm.enable || jackett.Anifilm.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string url) + { + if (!jackett.Anifilm.enable) + return Content("disable"); + + var proxyManager = new ProxyManager("anifilm", jackett.Anifilm); + + var fullNews = await Http.Get($"{jackett.Anifilm.host}/{url}", proxy: proxyManager.Get()); + if (fullNews == null) + return Content("error"); + + string tid = null; + string[] releasetorrents = fullNews.Split("
  • "); + + string _rnews = releasetorrents.FirstOrDefault(i => i.Contains("href=\"/releases/download-torrent/") && i.Contains(" 1080p ")); + if (!string.IsNullOrWhiteSpace(_rnews)) + tid = Regex.Match(_rnews, "href=\"/(releases/download-torrent/[0-9]+)\">скачать").Groups[1].Value; + + if (string.IsNullOrWhiteSpace(tid)) + tid = Regex.Match(fullNews, "href=\"/(releases/download-torrent/[0-9]+)\">скачать").Groups[1].Value; + + if (!string.IsNullOrWhiteSpace(tid)) + { + var _t = await Http.Download($"{jackett.Anifilm.host}/{tid}", referer: $"{jackett.Anifilm.host}/", proxy: proxyManager.Get()); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + } + + proxyManager.Refresh(); + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query) + { + #region html + var proxyManager = new ProxyManager("anifilm", jackett.Anifilm); + + string html = await Http.Get($"{jackett.Anifilm.host}/releases?title={HttpUtility.UrlEncode(query)}", timeoutSeconds: jackett.timeoutSeconds, proxy: proxyManager.Get()); + + if (html == null || !html.Contains("id=\"ui-components\"")) + { + consoleErrorLog("anifilm"); + proxyManager.Refresh(); + return null; + } + #endregion + + var torrents = new List(); + + if (html.Contains("class=\"releases__item\"")) + { + foreach (string row in html.Split("class=\"releases__item\"").Skip(1)) + { + if (string.IsNullOrWhiteSpace(row)) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Данные раздачи + string url = Match("]+>([^<]+)"); + string originalname = Match("([^<]+)"); + string episodes = Match("([0-9]+(-[0-9]+)?) из [0-9]+ эп.,"); + + if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(originalname)) + continue; + + int.TryParse(Match("([0-9]{4}) г\\."), out int relased); + + string title = $"{name} / {originalname}"; + + if (!string.IsNullOrWhiteSpace(episodes)) + title += $" ({episodes})"; + + var createTime = DateTime.Now.AddYears(-1); + + if (relased > 0) + { + title += $" [{relased}]"; + createTime = tParse.ParseCreateTime($"01.01.{relased}", "dd.MM.yyyy"); + } + #endregion + + torrents.Add(new TorrentDetails() + { + types = new string[] { "anime" }, + url = $"{jackett.Anifilm.host}/{url}", + title = title, + sid = 1, + createTime = createTime, + parselink = $"{host}/anifilm/parsemagnet?url={HttpUtility.UrlEncode(url)}", + name = name, + originalname = originalname, + relased = relased + }); + } + } + else + { + string url = Regex.Match(html, "property=\"og:url\" content=\"https?://[^/]+/([^\"]+)\"").Groups[1].Value; + string name = Regex.Match(html, "itemprop=\"name\">([^<]+)").Groups[1].Value; + string alternative = Regex.Match(html, "itemprop=\"alternativeHeadline\">([^<]+)").Groups[1].Value; + + if (!string.IsNullOrEmpty(name)) + { + torrents.Add(new TorrentDetails() + { + types = new string[] { "anime" }, + url = $"{jackett.Anifilm.host}/{url}", + title = name + (!string.IsNullOrEmpty(alternative) ? $" / {alternative}" : ""), + sid = 1, + parselink = $"{host}/anifilm/parsemagnet?url={HttpUtility.UrlEncode(url)}" + }); + } + } + + return torrents; + } + #endregion + } +} diff --git a/JacRed/Controllers/AnimeLayerController.cs b/JacRed/Controllers/AnimeLayerController.cs new file mode 100644 index 0000000..451069c --- /dev/null +++ b/JacRed/Controllers/AnimeLayerController.cs @@ -0,0 +1,219 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace JacRed.Controllers +{ + [Route("animelayer/[action]")] + public class AnimeLayerController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query) + { + if (!jackett.Animelayer.enable || string.IsNullOrEmpty(jackett.Animelayer.cookie ?? jackett.Animelayer.login.u) || jackett.Animelayer.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string url) + { + if (!jackett.Animelayer.enable) + return Content("disable"); + + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + return Content("cookie == null"); + + var proxyManager = new ProxyManager("animelayer", jackett.Animelayer); + + byte[] _t = await Http.Download($"{url}download/", proxy: proxyManager.Get(), cookie: cookie, referer: jackett.Animelayer.host); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query) + { + #region Авторизация + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + { + consoleErrorLog("animelayer"); + return null; + } + #endregion + + var torrents = new List(); + var proxyManager = new ProxyManager("animelayer", jackett.Animelayer); + + #region html + string html = await Http.Get($"{jackett.Animelayer.host}/torrents/anime/?q={HttpUtility.UrlEncode(query)}", proxy: proxyManager.Get(), cookie: cookie, timeoutSeconds: jackett.timeoutSeconds); + + if (html != null && html.Contains("id=\"wrapper\"")) + { + if (!html.Contains($">{jackett.Animelayer.login.u}<")) + { + consoleErrorLog("animelayer"); + return null; + } + } + else if (html == null) + { + consoleErrorLog("animelayer"); + return null; + } + #endregion + + foreach (string row in html.Split("class=\"torrent-item torrent-item-medium panel\"").Skip(1)) + { + if (string.IsNullOrWhiteSpace(row)) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim(); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Дата создания + DateTime createTime = default; + + if (Regex.IsMatch(row, "(Добавл|Обновл)[^<]+( )?[0-9]+ [^ ]+ [0-9]{4}")) + { + createTime = tParse.ParseCreateTime(Match(">(Добавл|Обновл)[^<]+( )?([0-9]+ [^ ]+ [0-9]{4})", 3), "dd.MM.yyyy"); + } + else + { + string date = Match("(Добавл|Обновл)[^<]+([^\n]+) в", 2); + if (!string.IsNullOrWhiteSpace(date)) + createTime = tParse.ParseCreateTime($"{date} {DateTime.Today.Year}", "dd.MM.yyyy"); + } + #endregion + + #region Данные раздачи + var gurl = Regex.Match(row, "([^<]+)").Groups; + + string url = gurl[1].Value; + string title = gurl[2].Value; + + string _sid = Match("class=\"icon s-icons-upload\">( )?([0-9]+)", 2); + string _pir = Match("class=\"icon s-icons-download\">( )?([0-9]+)", 2); + string sizeName = Match("[^<]+[^<]+[\n\r\t ]+([^\n\r<]+)").Trim(); + + if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(title)) + continue; + + if (Regex.IsMatch(row, "Разрешение: ?1920x1080")) + title += " [1080p]"; + else if (Regex.IsMatch(row, "Разрешение: ?1280x720")) + title += " [720p]"; + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = new string[] { "anime" }, + url = $"{jackett.Animelayer.host}/{url}/", + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/animelayer/parsemagnet?url={HttpUtility.UrlEncode(url)}" + }); + } + + return torrents; + } + #endregion + + + #region getCookie + async static ValueTask getCookie() + { + if (!string.IsNullOrEmpty(jackett.Animelayer.cookie)) + return jackett.Animelayer.cookie; + + string authKey = "Animelayer:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out string _cookie)) + return _cookie; + + if (Startup.memoryCache.TryGetValue($"{authKey}:error", out _)) + return null; + + Startup.memoryCache.Set($"{authKey}:error", 0, TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "login", jackett.Animelayer.login.u }, + { "password", jackett.Animelayer.login.p } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync($"{jackett.Animelayer.host}/auth/login/", postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string layer_id = null, layer_hash = null, PHPSESSID = null; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (line.Contains("layer_id=")) + layer_id = new Regex("layer_id=([^;]+)(;|$)").Match(line).Groups[1].Value; + + if (line.Contains("layer_hash=")) + layer_hash = new Regex("layer_hash=([^;]+)(;|$)").Match(line).Groups[1].Value; + + if (line.Contains("PHPSESSID=")) + PHPSESSID = new Regex("PHPSESSID=([^;]+)(;|$)").Match(line).Groups[1].Value; + } + + if (!string.IsNullOrWhiteSpace(layer_id) && !string.IsNullOrWhiteSpace(layer_hash) && !string.IsNullOrWhiteSpace(PHPSESSID)) + { + string cookie = $"layer_id={layer_id}; layer_hash={layer_hash}; PHPSESSID={PHPSESSID};"; + Startup.memoryCache.Set(authKey, cookie, DateTime.Today.AddDays(1)); + return cookie; + } + } + } + } + } + } + } + catch { } + + return null; + } + #endregion + } +} diff --git a/JacRed/Controllers/BigFanGroup.cs b/JacRed/Controllers/BigFanGroup.cs new file mode 100644 index 0000000..fce2cee --- /dev/null +++ b/JacRed/Controllers/BigFanGroup.cs @@ -0,0 +1,153 @@ +using Microsoft.AspNetCore.Mvc; + +namespace JacRed.Controllers +{ + [Route("bigfangroup/[action]")] + public class BigFanGroup : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string[] cats) + { + if (!jackett.BigFanGroup.enable || jackett.BigFanGroup.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cats)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id) + { + if (!jackett.BigFanGroup.enable) + return Content("disable"); + + var proxyManager = new ProxyManager("bigfangroup", jackett.BigFanGroup); + + var _t = await Http.Download($"{jackett.BigFanGroup.host}/download.php?id={id}", proxy: proxyManager.Get(), referer: jackett.BigFanGroup.host); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string[] cats) + { + var torrents = new List(); + var proxyManager = new ProxyManager("bigfangroup", jackett.BigFanGroup); + + #region Кеш html + string html = await Http.Get($"{jackett.BigFanGroup.host}/browse.php?search=" + HttpUtility.UrlEncode(query), proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds); + + if (html == null || !html.Contains("id=\"searchinput\"")) + { + consoleErrorLog("bigfangroup"); + return null; + } + #endregion + + var doc = new HtmlDocument(); + doc.LoadHtml(html.Replace(" ", " ")); + + var nodes = doc.DocumentNode.SelectNodes("//tbody//tr"); + if (nodes == null || nodes.Count == 0) + return null; + + foreach (var row in nodes) + { + var hc = new HtmlCommon(row); + + #region Данные раздачи + string title = hc.NodeValue(".//a//b"); + + DateTime createTime = tParse.ParseCreateTime(hc.NodeValue(".//img[@src='pic/time.png']", "title").Split(" в")[0], "dd.MM.yyyy"); + string viewtopic = hc.Match("href=\"details.php\\?id=([0-9]+)"); + string tracker = hc.Match("href=\"browse.php\\?cat=([0-9]+)"); + string sid = hc.NodeValue(".//font[@color='#000000']"); + string pir = hc.Match("todlers=[0-9]+\">([0-9]+)"); + string sizeName = hc.NodeValue(".//td[contains(text(), 'GB') or contains(text(), 'MB')]"); + + if (string.IsNullOrEmpty(viewtopic) || string.IsNullOrEmpty(tracker) || string.IsNullOrEmpty(title) || title.Contains(" | КПК")) + continue; + #endregion + + #region types + string[] types = null; + switch (tracker) + { + case "13": + case "52": + case "33": + case "48": + case "21": + case "39": + case "18": + case "24": + case "36": + case "53": + case "19": + case "31": + case "29": + case "27": + case "22": + case "26": + case "23": + case "30": + types = new string[] { "movie" }; + break; + case "12": + case "20": + case "47": + types = new string[] { "multfilm" }; + types = new string[] { "multserial" }; + break; + case "11": + types = new string[] { "serial" }; + break; + case "49": + case "32": + case "28": + types = new string[] { "docuserial", "documovie" }; + break; + case "25": + types = new string[] { "tvshow" }; + break; + } + + if (cats != null) + { + if (types == null) + continue; + + bool isok = false; + foreach (string cat in cats) + { + if (types.Contains(cat)) + isok = true; + } + + if (!isok) + continue; + } + #endregion + + torrents.Add(new TorrentDetails() + { + types = types, + url = $"{jackett.BigFanGroup.host}/forum/viewtopic.php?t={viewtopic}", + title = title, + sid = HtmlCommon.Integer(sid), + pir = HtmlCommon.Integer(pir), + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/bigfangroup/parsemagnet?id={viewtopic}" + }); + } + + return torrents; + } + #endregion + } +} diff --git a/JacRed/Controllers/BitruController.cs b/JacRed/Controllers/BitruController.cs new file mode 100644 index 0000000..1b3857b --- /dev/null +++ b/JacRed/Controllers/BitruController.cs @@ -0,0 +1,151 @@ +using Microsoft.AspNetCore.Mvc; + +namespace JacRed.Controllers +{ + [Route("bitru/[action]")] + public class BitruController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string[] cats) + { + if (!jackett.Bitru.enable || jackett.Bitru.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cats)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id, bool usecache) + { + if (!jackett.Bitru.enable) + return Content("disable"); + + var proxyManager = new ProxyManager("bitru", jackett.Bitru); + + byte[] _t = await Http.Download($"{jackett.Bitru.host}/download.php?id={id}", referer: $"{jackett.Bitru}/details.php?id={id}", proxy: proxyManager.Get()); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + proxyManager.Refresh(); + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string[] cats) + { + #region html + var proxyManager = new ProxyManager("bitru", jackett.Bitru); + + string html = await Http.Get($"{jackett.Bitru.host}/browse.php?s={HttpUtility.HtmlEncode(query)}&sort=&tmp=&cat=&subcat=&year=&country=&sound=&soundtrack=&subtitles=#content", proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds); + + if (html == null || !html.Contains("id=\"logo\"")) + { + consoleErrorLog("bitru"); + proxyManager.Refresh(); + return null; + } + #endregion + + var torrents = new List(); + + foreach (string row in html.Split("
    Аниме")) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Дата создания + DateTime createTime = default; + + if (row.Contains("Сегодня")) + { + createTime = DateTime.Today; + } + else if (row.Contains("Вчера")) + { + createTime = DateTime.Today.AddDays(-1); + } + else + { + createTime = tParse.ParseCreateTime(Match("
    (]+>)?([0-9]{2} [^ ]+ [0-9]{4}) в [0-9]{2}:[0-9]{2} от ([^<]+)
    "); + string _sid = Match("([0-9]+)"); + string _pir = Match("([0-9]+)"); + string sizeName = Match("title=\"Размер\">([^<]+)"); + + if (string.IsNullOrWhiteSpace(cat) || string.IsNullOrWhiteSpace(newsid) || string.IsNullOrWhiteSpace(title)) + continue; + + if (!title.ToLower().Contains(query.ToLower())) + continue; + #endregion + + #region types + string[] types = null; + switch (cat) + { + case "movie": + types = new string[] { "movie" }; + break; + case "serial": + types = new string[] { "serial" }; + break; + } + + if (cats != null) + { + if (types == null) + continue; + + bool isok = false; + foreach (string c in cats) + { + if (types.Contains(c)) + isok = true; + } + + if (!isok) + continue; + } + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = types, + url = $"{jackett.Bitru.host}/{url}", + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/bitru/parsemagnet?id={newsid}" + }); + } + + return torrents; + } + #endregion + } +} diff --git a/JacRed/Controllers/KinozalController.cs b/JacRed/Controllers/KinozalController.cs new file mode 100644 index 0000000..23fe25a --- /dev/null +++ b/JacRed/Controllers/KinozalController.cs @@ -0,0 +1,269 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace JacRed.Controllers +{ + [Route("kinozal/[action]")] + public class KinozalController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string[] cats) + { + if (!jackett.Kinozal.enable || jackett.Kinozal.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cats)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id) + { + if (!jackett.Kinozal.enable) + return Content("disable"); + + var proxyManager = new ProxyManager("kinozal", jackett.Kinozal); + + #region Download + if (jackett.Kinozal.cookie != null || Cookie != null) + { + var _t = await Http.Download("http://dl.kinozal.tv/download.php?id=" + id, proxy: proxyManager.Get(), cookie: jackett.Kinozal.cookie ?? Cookie, referer: jackett.Kinozal.host, timeoutSeconds: 10); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + } + #endregion + + string srv_details = await Http.Post($"{jackett.Kinozal.host}/get_srv_details.php?id={id}&action=2", $"id={id}&action=2", "__cfduid=d476ac2d9b5e18f2b67707b47ebd9b8cd1560164391; uid=20520283; pass=ouV5FJdFCd;", proxy: proxyManager.Get(), timeoutSeconds: 10); + if (srv_details != null) + { + string torrentHash = new Regex("
    • Инфо хеш: +([^<]+)
    • ").Match(srv_details).Groups[1].Value; + if (!string.IsNullOrEmpty(torrentHash)) + return Redirect($"magnet:?xt=urn:btih:{torrentHash}"); + } + + proxyManager.Refresh(); + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string[] cats) + { + var torrents = new List(); + var proxyManager = new ProxyManager("kinozal", jackett.Kinozal); + + string html = await Http.Get($"{jackett.Kinozal.host}/browse.php?s={HttpUtility.UrlEncode(query)}&g=0&c=0&v=0&d=0&w=0&t=0&f=0", proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds); + + if (html != null && html.Contains("Кинозал.ТВ")) + { + if (!html.Contains(">Выход") && !string.IsNullOrWhiteSpace(jackett.Kinozal.login.u) && !string.IsNullOrWhiteSpace(jackett.Kinozal.login.p)) + TakeLogin(); + } + else if (html == null) + { + consoleErrorLog("kinozal"); + proxyManager.Refresh(); + return null; + } + + foreach (string row in Regex.Split(html, "").Skip(1)) + { + if (string.IsNullOrWhiteSpace(row)) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Дата создания + DateTime createTime = default; + + if (row.Contains("сегодня")) + { + createTime = DateTime.Today; + } + else if (row.Contains("вчера")) + { + createTime = DateTime.Today.AddDays(-1); + } + else + { + createTime = tParse.ParseCreateTime(Match("([0-9]{2}.[0-9]{2}.[0-9]{4}) в [0-9]{2}:[0-9]{2}"), "dd.MM.yyyy"); + } + #endregion + + #region Данные раздачи + string url = Match("href=\"/(details.php\\?id=[0-9]+)\""); + string tracker = Match("src=\"/pic/cat/([0-9]+)\\.gif\""); + string title = Match("class=\"r[0-9]+\">([^<]+)"); + string _sid = Match("([0-9]+)"); + string _pir = Match("([0-9]+)"); + string sizeName = Match("([0-9\\.,]+ (МБ|ГБ))"); + + if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(tracker)) + continue; + #endregion + + // Id новости + string id = Match("href=\"/details.php\\?id=([0-9]+)\""); + if (string.IsNullOrEmpty(id)) + continue; + + #region types + string[] types = new string[] { }; + switch (tracker) + { + case "1002": + case "8": + case "6": + case "15": + case "17": + case "35": + case "39": + case "13": + case "14": + case "24": + case "11": + case "10": + case "9": + case "47": + case "18": + case "37": + case "12": + case "7": + case "16": + types = new string[] { "movie" }; + break; + case "45": + case "46": + types = new string[] { "serial" }; + break; + case "21": + case "22": + types = new string[] { "multfilm", "multserial" }; + break; + case "20": + types = new string[] { "anime" }; + break; + case "1006": + case "48": + case "49": + case "50": + case "38": + types = new string[] { "tvshow" }; + break; + } + + if (cats != null) + { + if (types == null) + continue; + + bool isok = false; + foreach (string cat in cats) + { + if (types.Contains(cat)) + isok = true; + } + + if (!isok) + continue; + } + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = types, + url = $"{jackett.Kinozal.host}/{url}", + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/kinozal/parsemagnet?id={id}" + }); + } + + return torrents; + } + #endregion + + + #region Cookie / TakeLogin + static string Cookie; + + async static void TakeLogin() + { + string authKey = "kinozal:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out _)) + return; + + Startup.memoryCache.Set(authKey, 0, AppInit.conf.multiaccess ? TimeSpan.FromMinutes(2) : TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + client.DefaultRequestHeaders.Add("origin", jackett.Kinozal.host); + client.DefaultRequestHeaders.Add("referer", $"{jackett.Kinozal.host}/"); + client.DefaultRequestHeaders.Add("upgrade-insecure-requests", "1"); + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "username", jackett.Kinozal.login.u }, + { "password", jackett.Kinozal.login.p }, + { "returnto", "" } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync($"{jackett.Kinozal.host}/takelogin.php", postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string uid = null, pass = null; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (line.Contains("uid=")) + uid = new Regex("uid=([0-9]+)").Match(line).Groups[1].Value; + + if (line.Contains("pass=")) + pass = new Regex("pass=([^;]+)(;|$)").Match(line).Groups[1].Value; + } + + if (!string.IsNullOrWhiteSpace(uid) && !string.IsNullOrWhiteSpace(pass)) + Cookie = $"uid={uid}; pass={pass};"; + } + } + } + } + } + } + catch { } + } + #endregion + } +} diff --git a/JacRed/Controllers/LostfilmController.cs b/JacRed/Controllers/LostfilmController.cs new file mode 100644 index 0000000..870c037 --- /dev/null +++ b/JacRed/Controllers/LostfilmController.cs @@ -0,0 +1,235 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace JacRed.Controllers +{ + [Route("lostfilm/[action]")] + public class LostfilmController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query) + { + if (!jackett.Lostfilm.enable || string.IsNullOrEmpty(jackett.Lostfilm.cookie ?? jackett.Lostfilm.login.u) || jackett.Lostfilm.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string episodeid) + { + if (!jackett.Lostfilm.enable) + return Content("disable"); + + var _t = await getTorrent(episodeid); + if (_t != null) + return File(_t, "application/x-bittorrent"); + + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query) + { + var proxyManager = new ProxyManager("lostfilm", jackett.Lostfilm); + + #region html + bool validrq = false; + string html = await Http.Get($"{jackett.Lostfilm.host}/search/?q={HttpUtility.UrlEncode(query)}", timeoutSeconds: jackett.timeoutSeconds, proxy: proxyManager.Get()); + + if (html != null && html.Contains("onClick=\"FollowSerial(")) + { + string serie = Regex.Match(html, "href=\"/series/([^\"]+)\" class=\"no-decoration\"").Groups[1].Value; + if (!string.IsNullOrWhiteSpace(serie)) + { + html = await Http.Get($"{jackett.Lostfilm.host}/series/{serie}/seasons/", timeoutSeconds: jackett.timeoutSeconds); + if (html != null && html.Contains("LostFilm.TV")) + validrq = true; + } + } + + if (!validrq) + { + consoleErrorLog("lostfilm"); + return null; + } + #endregion + + var torrents = new List(); + + foreach (string row in html.Split("").Skip(1)) + { + if (string.IsNullOrWhiteSpace(row)) + continue; + + #region Локальный метод - Match + string Match(string val, string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(val).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Данные раздачи + DateTime createTime = tParse.ParseCreateTime(Match(row, "data-released=\"([0-9]{2}\\.[0-9]{2}\\.[0-9]{4})\">([^<]+)"), "dd.MM.yyyy"); + + string url = Match(html, "href=\"/(series/[^/]+/seasons)\" class=\"item active\">Гид по сериям"); + string sinfo = Match(row, "title=\"Перейти к серии\">([^<]+)"); + string name = Match(html, "

      ([^<]+)

      "); + string originalname = Match(html, "

      ([^<]+)

      "); + + if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(originalname) || string.IsNullOrWhiteSpace(sinfo)) + continue; + #endregion + + string episodeid = Match(row, "onclick=\"PlayEpisode\\('([0-9]+)'\\)\""); + if (string.IsNullOrWhiteSpace(episodeid)) + continue; + + torrents.Add(new TorrentDetails() + { + types = new string[] { "serial" }, + url = $"{jackett.Lostfilm.host}/{url}", + title = $"{name} / {originalname} / {sinfo} [{createTime.Year}, 1080p]", + sid = 1, + createTime = createTime, + parselink = $"{host}/lostfilm/parsemagnet?episodeid={episodeid}", + name = name, + originalname = originalname, + relased = createTime.Year + }); + } + + return torrents; + } + #endregion + + + #region getTorrent + async Task getTorrent(string episodeid) + { + try + { + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + return null; + + var proxyManager = new ProxyManager("lostfilm", jackett.Lostfilm); + var proxy = proxyManager.Get(); + + // Получаем ссылку на поиск + string v_search = await Http.Get($"{jackett.Lostfilm.host}/v_search.php?a={episodeid}", proxy: proxy, cookie: cookie); + string retreSearchUrl = new Regex("url=(\")?(https?://[^/]+/[^\"]+)").Match(v_search ?? "").Groups[2].Value.Trim(); + if (!string.IsNullOrWhiteSpace(retreSearchUrl)) + { + // Загружаем HTML поиска + string shtml = await Http.Get(retreSearchUrl, proxy: proxy, cookie: cookie); + if (!string.IsNullOrWhiteSpace(shtml)) + { + var match = new Regex("").Match(Regex.Replace(shtml, "[\n\r\t]+", "")); + while (match.Success) + { + if (Regex.IsMatch(match.Groups[2].Value, "(2160p|2060p|1440p|1080p|720p)", RegexOptions.IgnoreCase)) + { + string torrentFile = match.Groups[1].Value; + string quality = Regex.Match(match.Groups[2].Value, "(2160p|2060p|1440p|1080p|720p)").Groups[1].Value; + + if (!string.IsNullOrWhiteSpace(torrentFile) && !string.IsNullOrWhiteSpace(quality)) + { + byte[] torrent = await Http.Download(torrentFile, referer: $"{jackett.Lostfilm.host}/", proxy: proxy, cookie: cookie); + if (BencodeTo.Magnet(torrent) != null) + return torrent; + } + } + + match = match.NextMatch(); + } + } + } + } + catch { } + + return null; + } + #endregion + + #region getCookie + async static ValueTask getCookie() + { + if (!string.IsNullOrEmpty(jackett.Lostfilm.cookie)) + return jackett.Lostfilm.cookie; + + string authKey = "Lostfilm:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out string _cookie)) + return _cookie; + + if (Startup.memoryCache.TryGetValue($"{authKey}:error", out _)) + return null; + + Startup.memoryCache.Set($"{authKey}:error", 0, TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "act", "users" }, + { "type", "login" }, + { "mail", jackett.Lostfilm.login.u }, + { "pass", jackett.Lostfilm.login.p }, + { "need_captcha", "" }, + { "captcha", "" }, + { "rem", "1" } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync($"{jackett.Lostfilm.host}/ajaxik.users.php", postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string cookie = string.Empty; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + cookie += " " + line; + } + + if (cookie.Contains("lf_session=") && cookie.Contains("lnk_uid=")) + { + cookie = Regex.Replace(cookie.Trim(), ";$", ""); + Startup.memoryCache.Set(authKey, cookie, DateTime.Today.AddDays(1)); + return cookie; + } + } + } + } + } + } + } + catch { } + + return null; + } + #endregion + } +} diff --git a/JacRed/Controllers/MegapeerController.cs b/JacRed/Controllers/MegapeerController.cs new file mode 100644 index 0000000..715c4fc --- /dev/null +++ b/JacRed/Controllers/MegapeerController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Mvc; +using System.Text; + +namespace JacRed.Controllers +{ + [Route("megapeer/[action]")] + public class MegapeerController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string cat) + { + if (!jackett.Megapeer.enable || jackett.Megapeer.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cat)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id) + { + if (!jackett.Megapeer.enable) + return Content("disable"); + + var proxyManager = new ProxyManager("megapeer", jackett.Megapeer); + + byte[] _t = await Http.Download($"{jackett.Megapeer.host}/download/{id}", referer: jackett.Megapeer.host, proxy: proxyManager.Get()); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + proxyManager.Refresh(); + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string cat) + { + #region html + var proxyManager = new ProxyManager("megapeer", jackett.Megapeer); + + string html = await Http.Get($"{jackett.Megapeer.host}/browse.php?search={HttpUtility.UrlEncode(query, Encoding.GetEncoding(1251))}&cat={cat}", encoding: Encoding.GetEncoding(1251), proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds, headers: HeadersModel.Init( + ("dnt", "1"), + ("pragma", "no-cache"), + ("referer", $"{jackett.Megapeer.host}"), + ("sec-fetch-dest", "document"), + ("sec-fetch-mode", "navigate"), + ("sec-fetch-site", "same-origin"), + ("sec-fetch-user", "?1"), + ("upgrade-insecure-requests", "1") + )); + + if (html == null || !html.Contains("id=\"logo\"") || html.Contains("

      Раздачи за последние")) + { + consoleErrorLog("megapeer"); + proxyManager.Refresh(); + return null; + } + #endregion + + var doc = new HtmlDocument(); + doc.LoadHtml(html.Replace(" ", " ")); + + var nodes = doc.DocumentNode.SelectNodes("//tr[@class='table_fon']"); + if (nodes == null || nodes.Count == 0) + return null; + + var torrents = new List(); + + foreach (var row in nodes) + { + var hc = new HtmlCommon(row); + + string url = hc.Match("href=\"/(torrent/[^\"]+)\""); + string title = hc.NodeValue(".//a[@class='url']"); + title = Regex.Replace(title, "<[^>]+>", ""); + + string sizeName = hc.NodeValue(".//td[@align='right' and contains(text(), 'GB') or contains(text(), 'MB')]"); + string downloadid = hc.Match("href=\"/?download/([0-9]+)\""); + string createTime = hc.NodeValue(".//td"); + + if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(downloadid)) + continue; + + torrents.Add(new TorrentDetails() + { + url = $"{jackett.Megapeer.host}/{url}", + title = title, + sid = HtmlCommon.Integer(hc.NodeValue(".//font[@color='#008000']")), + pir = HtmlCommon.Integer(hc.NodeValue(".//font[@color='#8b0000']")), + sizeName = sizeName, + parselink = $"{host}/megapeer/parsemagnet?id={downloadid}", + createTime = tParse.ParseCreateTime(createTime, "dd.MM.yy") + }); + } + + return torrents; + } + #endregion + } +} diff --git a/JacRed/Controllers/NNMClubController.cs b/JacRed/Controllers/NNMClubController.cs new file mode 100644 index 0000000..33c7747 --- /dev/null +++ b/JacRed/Controllers/NNMClubController.cs @@ -0,0 +1,279 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using System.Text; + +namespace JacRed.Controllers +{ + [Route("nnmclub/[action]")] + public class NNMClubController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string[] cats) + { + if (!jackett.NNMClub.enable || jackett.NNMClub.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cats)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id) + { + if (!jackett.NNMClub.enable) + return Content("disable"); + + var proxyManager = new ProxyManager("nnmclub", jackett.NNMClub); + + #region html + string html = await Http.Get($"{jackett.NNMClub.host}/forum/viewtopic.php?t=" + id, proxy: proxyManager.Get()); + string magnet = new Regex("href=\"(magnet:[^\"]+)\" title=\"Примагнититься\"").Match(html ?? string.Empty).Groups[1].Value; + + if (html == null) + { + proxyManager.Refresh(); + return Content("error"); + } + #endregion + + #region download torrent + if (jackett.NNMClub.cookie != null || Cookie != null) + { + string downloadid = new Regex("href=\"download\\.php\\?id=([0-9]+)\"").Match(html).Groups[1].Value; + if (!string.IsNullOrWhiteSpace(downloadid)) + { + byte[] _t = await Http.Download($"{jackett.NNMClub.host}/forum/download.php?id={downloadid}", proxy: proxyManager.Get(), cookie: jackett.NNMClub.cookie ?? Cookie, referer: jackett.NNMClub.host); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + } + } + #endregion + + if (string.IsNullOrEmpty(magnet)) + { + proxyManager.Refresh(); + return Content("error"); + } + + return Redirect(magnet); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string[] cats) + { + var torrents = new List(); + var proxyManager = new ProxyManager("nnmclub", jackett.NNMClub); + + #region html + string data = $"prev_sd=0&prev_a=0&prev_my=0&prev_n=0&prev_shc=0&prev_shf=1&prev_sha=1&prev_shs=0&prev_shr=0&prev_sht=0&o=1&s=2&tm=-1&shf=1&sha=1&ta=-1&sns=-1&sds=-1&nm={HttpUtility.UrlEncode(query, Encoding.GetEncoding(1251))}&pn=&submit=%CF%EE%E8%F1%EA"; + string html = await Http.Post($"{jackett.NNMClub.host}/forum/tracker.php", new System.Net.Http.StringContent(data, Encoding.UTF8, "application/x-www-form-urlencoded"), encoding: Encoding.GetEncoding(1251), proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds); + + if (html != null && html.Contains("NNM-Club")) + { + if (!html.Contains(">Выход") && !string.IsNullOrWhiteSpace(jackett.NNMClub.login.u) && !string.IsNullOrWhiteSpace(jackett.NNMClub.login.p)) + TakeLogin(); + } + else if (html == null) + { + consoleErrorLog("nnmclub"); + proxyManager.Refresh(); + return null; + } + #endregion + + foreach (string row in html.Split("")) + { + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Данные раздачи + string url = Match("href=\"(viewtopic.php\\?t=[0-9]+)\""); + string viewtopic = Match("href=\"viewtopic.php\\?t=([0-9]+)\""); + string tracker = Match("class=\"gen\" href=\"tracker.php\\?f=([0-9]+)"); + + string title = Match("class=\"genmed topictitle\" [^>]+>([^<]+)"); + string _sid = Match("class=\"seedmed\">([0-9]+)<"); + string _pir = Match("class=\"leechmed\">([0-9]+)"); + string sizeName = Match("class=\"gensmall\">[^<]+ ([^<]+)"); + + if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(viewtopic) || string.IsNullOrWhiteSpace(tracker)) + continue; + + if (tracker == "913" && !title.Contains("UKR")) + continue; + #endregion + + #region types + string[] types = null; + switch (tracker) + { + case "270": + case "221": + case "882": + case "225": + case "227": + case "913": + case "218": + case "954": + case "1293": + case "1296": + case "1299": + case "682": + case "884": + case "693": + types = new string[] { "movie" }; + break; + case "769": + case "768": + types = new string[] { "serial" }; + break; + case "713": + case "576": + case "610": + types = new string[] { "docuserial", "documovie" }; + break; + case "731": + case "733": + case "1329": + case "1330": + case "1331": + case "1332": + case "1336": + case "1337": + case "1338": + case "1339": + types = new string[] { "multfilm" }; + break; + case "658": + case "232": + types = new string[] { "multserial" }; + break; + case "623": + case "622": + case "621": + case "632": + case "627": + case "626": + case "625": + case "644": + types = new string[] { "anime" }; + break; + } + + if (cats != null) + { + if (types == null) + continue; + + bool isok = false; + foreach (string cat in cats) + { + if (types.Contains(cat)) + isok = true; + } + + if (!isok) + continue; + } + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = types, + url = $"{jackett.NNMClub.host}/{url}", + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + parselink = $"{host}/nnmclub/parsemagnet?id={viewtopic}", + createTime = tParse.ParseCreateTime(Match("title=\"Добавлено\" class=\"gensmall\">[0-9]+ ([0-9]{2}-[0-9]{2}-[0-9]{4}
      [^<]+)").Replace("
      ", " "), "dd-MM-yyyy HH:mm") + }); + } + + return torrents; + } + #endregion + + + #region Cookie / TakeLogin + static string Cookie; + + async static void TakeLogin() + { + string authKey = "nnmclub:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out _)) + return; + + Startup.memoryCache.Set(authKey, 0, AppInit.conf.multiaccess ? TimeSpan.FromMinutes(2) : TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + client.DefaultRequestHeaders.Add("origin", jackett.NNMClub.host); + client.DefaultRequestHeaders.Add("referer", $"{jackett.NNMClub.host}/"); + client.DefaultRequestHeaders.Add("upgrade-insecure-requests", "1"); + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "redirect", "%2F" }, + { "username", jackett.NNMClub.login.u }, + { "password", jackett.NNMClub.login.p }, + { "autologin", "on" }, + { "login", "%C2%F5%EE%E4" } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync($"{jackett.NNMClub.host}/forum/login.php", postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string data = null, sid = null; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (line.Contains("phpbb2mysql_4_data=")) + data = new Regex("phpbb2mysql_4_data=([^;]+)(;|$)").Match(line).Groups[1].Value; + + if (line.Contains("phpbb2mysql_4_sid=")) + sid = new Regex("phpbb2mysql_4_sid=([^;]+)(;|$)").Match(line).Groups[1].Value; + } + + if (!string.IsNullOrWhiteSpace(data) && !string.IsNullOrWhiteSpace(sid)) + Cookie = $"phpbb2mysql_4_data={data}; phpbb2mysql_4_sid={sid};"; + } + } + } + } + } + } + catch { } + } + #endregion + } +} diff --git a/JacRed/Controllers/RutorController.cs b/JacRed/Controllers/RutorController.cs new file mode 100644 index 0000000..42c5ecb --- /dev/null +++ b/JacRed/Controllers/RutorController.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Mvc; + +namespace JacRed.Controllers +{ + [Route("rutor/[action]")] + public class RutorController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string cat, bool isua = false, string parsecat = null) + { + if (!jackett.Rutor.enable || jackett.Rutor.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cat, isua, parsecat)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(int id, string magnet) + { + if (!jackett.Rutor.enable || jackett.Rutor.priority != "torrent") + return Content("disable"); + + var proxyManager = new ProxyManager("rutor", jackett.Rutor); + + byte[] _t = await Http.Download($"{Regex.Replace(jackett.Rutor.host, "^(https?:)//", "$1//d.")}/download/{id}", referer: jackett.Rutor.host, proxy: proxyManager.Get()); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + proxyManager.Refresh(); + + if (string.IsNullOrEmpty(magnet)) + return Content("empty"); + + return Redirect(magnet); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string cat, bool isua, string parsecat) + { + // fix search + query = query.Replace("\"", " ").Replace("'", " ").Replace("?", " ").Replace("&", " "); + + var proxyManager = new ProxyManager("rutor", jackett.Rutor); + + string html = await Http.Get($"{jackett.Rutor.host}/search" + (cat == "0" ? $"/{HttpUtility.UrlEncode(query)}" : $"/0/{cat}/000/0/{HttpUtility.UrlEncode(query)}"), proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds); + + if (html == null || !html.Contains("id=\"logo\"")) + { + consoleErrorLog("rutor"); + proxyManager.Refresh(); + return null; + } + + var doc = new HtmlDocument(); + doc.LoadHtml(html.Replace(" ", " ").Replace(" ", " ")); // Меняем непонятный символ похожий на проблел, на обычный проблел + + var nodes = doc.DocumentNode.SelectNodes("//tr[@class='gai' or @class='tum']"); + if (nodes == null || nodes.Count == 0) + return null; + + var torrents = new List(); + + foreach (var row in nodes) + { + var hc = new HtmlCommon(row); + + string url = hc.Match("href=\"/(torrent/[^\"]+)\""); + string viewtopic = Regex.Match(url, "torrent/([0-9]+)").Groups[1].Value; + + string title = hc.NodeValue(".//a[contains(@href, '/torrent/')]"); + string sid = hc.NodeValue(".//span[@class='green']", removeChild: ".//img"); + string pir = hc.NodeValue(".//span[@class='red']"); + string sizeName = hc.NodeValue(".//td[@align='right' and contains(text(), 'GB') or contains(text(), 'MB')]"); + string createTime = hc.NodeValue(".//td"); + string magnet = hc.Match("href=\"(magnet:\\?xt=[^\"]+)\""); + + if (string.IsNullOrEmpty(title) || string.IsNullOrEmpty(magnet) || title.ToLower().Contains("трейлер")) + continue; + + if (isua && !title.Contains(" UKR")) + continue; + + torrents.Add(new TorrentDetails() + { + url = $"{jackett.Rutor.host}/{url}", + title = title, + sid = HtmlCommon.Integer(sid), + pir = HtmlCommon.Integer(pir), + sizeName = sizeName, + magnet = jackett.Rutor.priority == "torrent" ? null : magnet, + parselink = jackett.Rutor.priority == "torrent" ? $"{host}/rutor/parsemagnet?id={viewtopic}&magnet={HttpUtility.UrlEncode(magnet)}" : null, + createTime = tParse.ParseCreateTime(createTime, "dd.MM.yy") + }); + } + + return torrents; + } + #endregion + } +} diff --git a/JacRed/Controllers/RutrackerController.cs b/JacRed/Controllers/RutrackerController.cs new file mode 100644 index 0000000..a3bcc35 --- /dev/null +++ b/JacRed/Controllers/RutrackerController.cs @@ -0,0 +1,450 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace JacRed.Controllers +{ + [Route("rutracker/[action]")] + public class RutrackerController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string[] cats) + { + if (!jackett.Rutracker.enable || string.IsNullOrEmpty(jackett.Rutracker.cookie ?? jackett.Rutracker.login.u) || jackett.Rutracker.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cats)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id) + { + if (!jackett.Rutracker.enable) + return Content("disable"); + + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + return Content("cookie == null"); + + var proxyManager = new ProxyManager("rutracker", jackett.Rutracker); + + #region Download + if (jackett.Rutracker.priority == "torrent") + { + var _t = await Http.Download($"{jackett.Rutracker.host}/forum/dl.php?t={id}", proxy: proxyManager.Get(), cookie: cookie, referer: jackett.Rutracker.host); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + } + #endregion + + #region Magnet + var fullNews = await Http.Get($"{jackett.Rutracker.host}/forum/viewtopic.php?t=" + id, proxy: proxyManager.Get(), cookie: cookie); + if (fullNews != null) + { + string magnet = Regex.Match(fullNews, "href=\"(magnet:[^\"]+)\" class=\"(med )?med magnet-link\"").Groups[1].Value; + if (!string.IsNullOrWhiteSpace(magnet)) + return Redirect(magnet); + } + #endregion + + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string[] cats) + { + var torrents = new List(); + var proxyManager = new ProxyManager("rutracker", jackett.Rutracker); + + #region Авторизация + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + { + consoleErrorLog("rutracker"); + return null; + } + #endregion + + #region Кеш html + string html = await Http.Get($"{jackett.Rutracker.host}/forum/tracker.php?nm=" + HttpUtility.UrlEncode(query), proxy: proxyManager.Get(), cookie: cookie, timeoutSeconds: jackett.timeoutSeconds); + + if (html != null) + { + if (!html.Contains("id=\"logged-in-username\"")) + { + consoleErrorLog("rutracker"); + return null; + } + } + #endregion + + foreach (string row in html.Split("class=\"tCenter hl-tr\"").Skip(1)) + { + if (string.IsNullOrWhiteSpace(row)) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Данные раздачи + string title = Match("href=\"viewtopic.php\\?t=[0-9]+\">([^\n\r]+)"); + title = Regex.Replace(title, "<[^>]+>", ""); + + DateTime createTime = tParse.ParseCreateTime(Match("

      ([0-9]{2}-[^-<]+-[0-9]{2})

      ").Replace("-", " "), "dd.MM.yy"); + string viewtopic = Match("href=\"viewtopic.php\\?t=([0-9]+)\""); + string tracker = Match("href=\"tracker.php\\?f=([0-9]+)"); + string _sid = Match("class=\"seedmed\">([0-9]+)"); + string _pir = Match("title=\"Личи\">([0-9]+)"); + string sizeName = Match("href=\"dl.php\\?t=[0-9]+\">([^<]+) ↓").Replace(" ", " "); + + if (string.IsNullOrWhiteSpace(viewtopic) || string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(tracker)) + continue; + #endregion + + #region types + string[] types = null; + switch (tracker) + { + case "22": + case "1666": + case "941": + case "1950": + case "2090": + case "2221": + case "2091": + case "2092": + case "2093": + case "2200": + case "2540": + case "934": + case "505": + case "124": + case "1457": + case "2199": + case "313": + case "312": + case "1247": + case "2201": + case "2339": + case "140": + case "252": + case "2198": + types = new string[] { "movie" }; + break; + case "2343": + case "930": + case "2365": + case "208": + case "539": + case "209": + types = new string[] { "multfilm" }; + break; + case "921": + case "815": + case "1460": + types = new string[] { "multserial" }; + break; + case "842": + case "235": + case "242": + case "819": + case "1531": + case "721": + case "1102": + case "1120": + case "1214": + case "489": + case "387": + case "9": + case "81": + case "119": + case "1803": + case "266": + case "193": + case "1690": + case "1459": + case "825": + case "1248": + case "1288": + case "325": + case "534": + case "694": + case "704": + case "915": + case "1939": + types = new string[] { "serial" }; + break; + case "1105": + case "2491": + case "1389": + types = new string[] { "anime" }; + break; + case "709": + types = new string[] { "documovie" }; + break; + case "46": + case "671": + case "2177": + case "2538": + case "251": + case "98": + case "97": + case "851": + case "2178": + case "821": + case "2076": + case "56": + case "2123": + case "876": + case "2139": + case "1467": + case "1469": + case "249": + case "552": + case "500": + case "2112": + case "1327": + case "1468": + case "2168": + case "2160": + case "314": + case "1281": + case "2110": + case "979": + case "2169": + case "2164": + case "2166": + case "2163": + types = new string[] { "docuserial", "documovie" }; + break; + case "24": + case "1959": + case "939": + case "1481": + case "113": + case "115": + case "882": + case "1482": + case "393": + case "2537": + case "532": + case "827": + types = new string[] { "tvshow" }; + break; + case "2103": + case "2522": + case "2485": + case "2486": + case "2479": + case "2089": + case "1794": + case "845": + case "2312": + case "343": + case "2111": + case "1527": + case "2069": + case "1323": + case "2009": + case "2000": + case "2010": + case "2006": + case "2007": + case "2005": + case "259": + case "2004": + case "1999": + case "2001": + case "2002": + case "283": + case "1997": + case "2003": + case "1608": + case "1609": + case "2294": + case "1229": + case "1693": + case "2532": + case "136": + case "592": + case "2533": + case "1952": + case "1621": + case "2075": + case "1668": + case "1613": + case "1614": + case "1623": + case "1615": + case "1630": + case "2425": + case "2514": + case "1616": + case "2014": + case "1442": + case "1491": + case "1987": + case "1617": + case "1620": + case "1998": + case "1343": + case "751": + case "1697": + case "255": + case "260": + case "261": + case "256": + case "1986": + case "660": + case "1551": + case "626": + case "262": + case "1326": + case "978": + case "1287": + case "1188": + case "1667": + case "1675": + case "257": + case "875": + case "263": + case "2073": + case "550": + case "2124": + case "1470": + case "528": + case "486": + case "854": + case "2079": + case "1336": + case "2171": + case "1339": + case "2455": + case "1434": + case "2350": + case "1472": + case "2068": + case "2016": + types = new string[] { "sport" }; + break; + } + + if (cats != null) + { + if (types == null) + continue; + + bool isok = false; + foreach (string cat in cats) + { + if (types.Contains(cat)) + isok = true; + } + + if (!isok) + continue; + } + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = types, + url = $"{jackett.Rutracker.host}/forum/viewtopic.php?t={viewtopic}", + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/rutracker/parsemagnet?id={viewtopic}" + }); + } + + return torrents; + } + #endregion + + + #region getCookie + async static ValueTask getCookie() + { + if (!string.IsNullOrEmpty(jackett.Rutracker.cookie)) + return jackett.Rutracker.cookie; + + string authKey = "Rutracker:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out string _cookie)) + return _cookie; + + if (Startup.memoryCache.TryGetValue($"{authKey}:error", out _)) + return null; + + Startup.memoryCache.Set($"{authKey}:error", 0, TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "login_username", jackett.Rutracker.login.u }, + { "login_password", jackett.Rutracker.login.p }, + { "login", "Вход" } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync($"{jackett.Rutracker.host}/forum/login.php", postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string session = null; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (line.Contains("bb_session=")) + session = new Regex("bb_session=([^;]+)(;|$)").Match(line).Groups[1].Value; + } + + if (!string.IsNullOrWhiteSpace(session)) + { + string cookie = $"bb_ssl=1; bb_session={session};"; + Startup.memoryCache.Set(authKey, cookie, DateTime.Today.AddDays(1)); + return cookie; + } + } + } + } + } + } + } + catch { } + + return null; + } + #endregion + } +} diff --git a/JacRed/Controllers/SelezenController.cs b/JacRed/Controllers/SelezenController.cs new file mode 100644 index 0000000..cc71240 --- /dev/null +++ b/JacRed/Controllers/SelezenController.cs @@ -0,0 +1,215 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace JacRed.Controllers +{ + [Route("selezen/[action]")] + public class SelezenController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query) + { + if (!jackett.Selezen.enable || string.IsNullOrEmpty(jackett.Selezen.cookie ?? jackett.Selezen.login.u) || jackett.Selezen.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string url) + { + if (!jackett.Selezen.enable) + return Content("disable"); + + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + return Content("cookie == null"); + + var proxyManager = new ProxyManager("selezen", jackett.Selezen); + + string html = await Http.Get(url, cookie: cookie, proxy: proxyManager.Get()); + string magnet = new Regex("href=\"(magnet:[^\"]+)\"").Match(html ?? string.Empty).Groups[1].Value; + + if (html == null) + return Content("error"); + + #region Download + if (jackett.Selezen.priority == "torrent") + { + string id = new Regex("href=\"/index.php\\?do=download&id=([0-9]+)").Match(html).Groups[1].Value; + if (!string.IsNullOrWhiteSpace(id)) + { + var _t = await Http.Download($"{jackett.Selezen.host}/index.php?do=download&id={id}", cookie: cookie, referer: jackett.Selezen.host, timeoutSeconds: 10); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + } + } + #endregion + + if (string.IsNullOrWhiteSpace(magnet)) + return Content("error"); + + return Redirect(magnet); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query) + { + #region Авторизация + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + { + consoleErrorLog("selezen"); + return null; + } + #endregion + + #region html + var proxyManager = new ProxyManager("selezen", jackett.Selezen); + + string html = await Http.Post($"{jackett.Selezen.host}/index.php?do=search", $"do=search&subaction=search&search_start=0&full_search=0&result_from=1&story={HttpUtility.UrlEncode(query)}&titleonly=0&searchuser=&replyless=0&replylimit=0&searchdate=0&beforeafter=after&sortby=date&resorder=desc&showposts=0&catlist%5B%5D=9", proxy: proxyManager.Get(), cookie: cookie, timeoutSeconds: jackett.timeoutSeconds); + + if (html != null && html.Contains("dle_root")) + { + if (!html.Contains($">{jackett.Selezen.login.u}<")) + { + consoleErrorLog("selezen"); + return null; + } + } + #endregion + + var torrents = new List(); + + foreach (string row in html.Split("class=\"card radius-10 overflow-hidden\"").Skip(1)) + { + if (string.IsNullOrWhiteSpace(row) || row.Contains(">Аниме") || row.Contains(" [S0")) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Данные раздачи + var g = Regex.Match(row, "

      ([^<]+)

      ").Groups; + string url = g[1].Value; + string title = g[2].Value; + + string _sid = Match("([0-9 ]+)").Trim(); + string _pir = Match("([0-9 ]+)").Trim(); + string sizeName = Match("([^<]+)
      ").Trim(); + DateTime createTime = tParse.ParseCreateTime(Match("class=\"bx bx-calendar\"> ?([0-9]{2}\\.[0-9]{2}\\.[0-9]{4} [0-9]{2}:[0-9]{2})"), "dd.MM.yyyy HH:mm"); + + if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(url)) + continue; + #endregion + + #region types + string[] types = new string[] { "movie" }; + if (row.Contains(">Мульт") || row.Contains(">мульт")) + types = new string[] { "multfilm" }; + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = types, + url = url, + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/selezen/parsemagnet?url={HttpUtility.UrlEncode(url)}" + }); + } + + return torrents; + } + #endregion + + + #region getCookie + async static ValueTask getCookie() + { + if (!string.IsNullOrEmpty(jackett.Selezen.cookie)) + return jackett.Selezen.cookie; + + string authKey = "selezen:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out string _cookie)) + return _cookie; + + if (Startup.memoryCache.TryGetValue($"{authKey}:error", out _)) + return null; + + Startup.memoryCache.Set($"{authKey}:error", 0, TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "login_name", jackett.Selezen.login.u }, + { "login_password", jackett.Selezen.login.p }, + { "login_not_save", "1" }, + { "login", "submit" } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync(jackett.Selezen.host, postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string PHPSESSID = null; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (line.Contains("PHPSESSID=")) + PHPSESSID = new Regex("PHPSESSID=([^;]+)(;|$)").Match(line).Groups[1].Value; + } + + if (!string.IsNullOrWhiteSpace(PHPSESSID)) + { + string cookie = $"PHPSESSID={PHPSESSID}; _ym_isad=2;"; + Startup.memoryCache.Set(authKey, cookie, DateTime.Today.AddDays(1)); + return cookie; + } + } + } + } + } + } + } + catch { } + + return null; + } + #endregion + } +} diff --git a/JacRed/Controllers/TolokaController.cs b/JacRed/Controllers/TolokaController.cs new file mode 100644 index 0000000..dc00016 --- /dev/null +++ b/JacRed/Controllers/TolokaController.cs @@ -0,0 +1,399 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace JacRed.Controllers +{ + [Route("toloka/[action]")] + public class TolokaController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string[] cats) + { + if (!jackett.Toloka.enable || string.IsNullOrEmpty(jackett.Toloka.cookie ?? jackett.Toloka.login.u) || jackett.Toloka.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cats)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(string id) + { + if (!jackett.Toloka.enable) + return Content("disable"); + + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + return Content("cookie == null"); + + var proxyManager = new ProxyManager("toloka", jackett.Toloka); + + byte[] _t = await Http.Download($"{jackett.Toloka.host}/download.php?id={id}", proxy: proxyManager.Get(), cookie: cookie, referer: jackett.Toloka.host); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + return Content("error"); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string[] cats) + { + #region Авторизация + string cookie = await getCookie(); + if (string.IsNullOrEmpty(cookie)) + { + consoleErrorLog("toloka"); + return null; + } + #endregion + + #region html + var proxyManager = new ProxyManager("toloka", jackett.Toloka); + + string html = await Http.Get($"{jackett.Toloka.host}/tracker.php?prev_sd=0&prev_a=0&prev_my=0&prev_n=0&prev_shc=0&prev_shf=1&prev_sha=1&prev_cg=0&prev_ct=0&prev_at=0&prev_nt=0&prev_de=0&prev_nd=0&prev_tcs=1&prev_shs=0&f%5B%5D=-1&o=1&s=2&tm=-1&shf=1&sha=1&tcs=1&sns=-1&sds=-1&nm={HttpUtility.UrlEncode(query)}&pn=&send=%D0%9F%D0%BE%D1%88%D1%83%D0%BA", proxy: proxyManager.Get(), cookie: cookie, timeoutSeconds: jackett.timeoutSeconds); + + if (html != null && html.Contains("Вихід")) + { + consoleErrorLog("toloka"); + return null; + } + } + #endregion + + var torrents = new List(); + + foreach (string row in html.Split("")) + { + if (string.IsNullOrWhiteSpace(row) || Regex.IsMatch(row, "Збір коштів", RegexOptions.IgnoreCase)) + continue; + + #region Локальный метод - Match + string Match(string pattern, int index = 1) + { + string res = HttpUtility.HtmlDecode(new Regex(pattern, RegexOptions.IgnoreCase).Match(row).Groups[index].Value.Trim()); + res = Regex.Replace(res, "[\n\r\t ]+", " "); + return res.Trim(); + } + #endregion + + #region Дата создания + string _createTime = Match("class=\"gensmall\">([0-9]{4}-[0-9]{2}-[0-9]{2})").Replace("-", "."); + DateTime.TryParse(_createTime, out DateTime createTime); + #endregion + + #region Данные раздачи + string url = Match("class=\"topictitle genmed\">]+>([^<]+)"); + string downloadid = Match("href=\"download.php\\?id=([0-9]+)\""); + string tracker = Match("class=\"gen\" href=\"tracker.php\\?f=([0-9]+)"); + string _sid = Match("class=\"seedmed\">([0-9]+)"); + string _pir = Match("class=\"leechmed\">([0-9]+)"); + string sizeName = Match("class=\"gensmall\">([0-9\\.]+ (MB|GB))"); + + if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(downloadid) || string.IsNullOrWhiteSpace(tracker) || sizeName == "0 B") + continue; + #endregion + + #region Парсим раздачи + int relased = 0; + string name = null, originalname = null; + + if (tracker is "16" or "96" or "19" or "139" or "12" or "131" or "84" or "42") + { + #region Фильмы + // Незворотність / Irréversible / Irreversible (2002) AVC Ukr/Fre | Sub Eng + var g = Regex.Match(title, "^([^/\\(\\[]+)/[^/\\(\\[]+/([^/\\(\\[]+) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value) && !string.IsNullOrWhiteSpace(g[3].Value)) + { + name = g[1].Value.Trim(); + originalname = g[2].Value.Trim(); + + if (int.TryParse(g[3].Value, out int _yer)) + relased = _yer; + } + else + { + // Мій рік у Нью-Йорку / My Salinger Year (2020) Ukr/Eng + g = Regex.Match(title, "^([^/\\(\\[]+)/([^/\\(\\[]+) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value) && !string.IsNullOrWhiteSpace(g[3].Value)) + { + name = g[1].Value.Trim(); + originalname = g[2].Value.Trim(); + + if (int.TryParse(g[3].Value, out int _yer)) + relased = _yer; + } + else + { + // Хроніка надій та ілюзій. Дзеркало історії. (83 серії) (2001-2003) PDTVRip + g = Regex.Match(title, "^([^/\\(\\[]+) \\([^\\)]+\\) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value)) + { + name = g[1].Value; + + if (int.TryParse(g[2].Value, out int _yer)) + relased = _yer; + } + else + { + // Берестечко. Битва за Україну (2015-2016) DVDRip-AVC + g = Regex.Match(title, "^([^/\\(\\[]+) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value)) + { + name = g[1].Value; + + if (int.TryParse(g[2].Value, out int _yer)) + relased = _yer; + } + } + } + } + #endregion + } + else if (tracker is "32" or "173" or "174" or "44" or "230" or "226" or "227" or "228" or "229" or "127" or "124" or "125" or "132") + { + #region Сериалы + // Атака титанів (Attack on Titan) (Сезон 1) / Shingeki no Kyojin (Season 1) (2013) BDRip 720р + var g = Regex.Match(title, "^([^/\\(\\[]+) \\([^\\)]+\\) \\([^\\)]+\\) ?/([^/\\(\\[]+) \\([^\\)]+\\) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value) && !string.IsNullOrWhiteSpace(g[3].Value)) + { + name = g[1].Value.Trim(); + originalname = g[2].Value.Trim(); + + if (int.TryParse(g[3].Value, out int _yer)) + relased = _yer; + } + else + { + // Дім з прислугою (Сезон 2, серії 1-8) / Servant (Season 2, episodes 1-8) (2021) WEB-DLRip-AVC Ukr/Eng + g = Regex.Match(title, "^([^/\\(\\[]+) \\([^\\)]+\\) ?/([^/\\(\\[]+) \\([^\\)]+\\) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value) && !string.IsNullOrWhiteSpace(g[3].Value)) + { + name = g[1].Value.Trim(); + originalname = g[2].Value.Trim(); + + if (int.TryParse(g[3].Value, out int _yer)) + relased = _yer; + } + else + { + // Детективне агентство прекрасних хлопчиків (08 з 12) / Bishounen Tanteidan (2021) BDRip 1080p Ukr/Jap | Ukr Sub + g = Regex.Match(title, "^([^/\\(\\[]+) (\\(|\\[)[^\\)\\]]+(\\)|\\]) ?/([^/\\(\\[]+) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[4].Value) && !string.IsNullOrWhiteSpace(g[5].Value)) + { + name = g[1].Value.Trim(); + originalname = g[4].Value.Trim(); + + if (int.TryParse(g[5].Value, out int _yer)) + relased = _yer; + } + else + { + // Яйця Дракона / Dragon Ball (01-31 з 153) (1986-1989) BDRip 1080p H.265 + // Томо — дівчина! / Tomo-chan wa Onnanoko! (Сезон 1, серії 01-02 з 13) (2023) WEBDL 1080p H.265 Ukr/Jap | sub Ukr + g = Regex.Match(title, "^([^/\\(\\[]+)/([^/\\(\\[]+) \\([^\\)]+\\) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value) && !string.IsNullOrWhiteSpace(g[3].Value)) + { + name = g[1].Value.Trim(); + originalname = g[2].Value.Trim(); + + if (int.TryParse(g[3].Value, out int _yer)) + relased = _yer; + } + else + { + // Людина-бензопила / チェンソーマン /Chainsaw Man (сезон 1, серії 8 з 12) (2022) WEBRip 1080p + g = Regex.Match(title, "^([^/\\(\\[]+)/[^/\\(\\[]+/([^/\\(\\[]+) \\([^\\)]+\\) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value) && !string.IsNullOrWhiteSpace(g[3].Value)) + { + name = g[1].Value.Trim(); + originalname = g[2].Value.Trim(); + + if (int.TryParse(g[3].Value, out int _yer)) + relased = _yer; + } + else + { + // МастерШеф. 10 сезон (1-18 епізоди) (2020) IPTVRip 400p + g = Regex.Match(title, "^([^/\\(\\[]+) \\([^\\)]+\\) \\(([0-9]{4})(\\)|-)").Groups; + if (!string.IsNullOrWhiteSpace(g[1].Value) && !string.IsNullOrWhiteSpace(g[2].Value)) + { + name = g[1].Value.Trim(); + + if (int.TryParse(g[2].Value, out int _yer)) + relased = _yer; + } + } + } + } + } + } + #endregion + } + #endregion + + #region types + string[] types = null; + switch (tracker) + { + case "16": + case "96": + case "42": + types = new string[] { "movie" }; + break; + case "19": + case "139": + case "84": + types = new string[] { "multfilm" }; + break; + case "32": + case "173": + case "124": + types = new string[] { "serial" }; + break; + case "174": + case "44": + case "125": + types = new string[] { "multserial" }; + break; + case "226": + case "227": + case "228": + case "229": + case "230": + case "12": + case "131": + types = new string[] { "docuserial", "documovie" }; + break; + case "127": + types = new string[] { "anime" }; + break; + case "132": + types = new string[] { "tvshow" }; + break; + } + + if (cats != null) + { + if (types == null) + continue; + + bool isok = false; + foreach (string cat in cats) + { + if (types.Contains(cat)) + isok = true; + } + + if (!isok) + continue; + } + #endregion + + int.TryParse(_sid, out int sid); + int.TryParse(_pir, out int pir); + + torrents.Add(new TorrentDetails() + { + types = types, + url = $"{jackett.Toloka.host}/{url}", + title = title, + sid = sid, + pir = pir, + sizeName = sizeName, + createTime = createTime, + parselink = $"{host}/toloka/parsemagnet?id={downloadid}", + name = name, + originalname = originalname, + relased = relased + }); + } + + return torrents; + } + #endregion + + + #region getCookie + async static ValueTask getCookie() + { + if (!string.IsNullOrEmpty(jackett.Toloka.cookie)) + return jackett.Toloka.cookie; + + string authKey = "Toloka:TakeLogin()"; + if (Startup.memoryCache.TryGetValue(authKey, out string _cookie)) + return _cookie; + + if (Startup.memoryCache.TryGetValue($"{authKey}:error", out _)) + return null; + + Startup.memoryCache.Set($"{authKey}:error", 0, TimeSpan.FromSeconds(20)); + + try + { + using (var clientHandler = new System.Net.Http.HttpClientHandler() + { + AllowAutoRedirect = false + }) + { + clientHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + using (var client = new System.Net.Http.HttpClient(clientHandler)) + { + client.Timeout = TimeSpan.FromSeconds(jackett.timeoutSeconds); + client.MaxResponseContentBufferSize = 2000000; // 2MB + + foreach (var h in Http.defaultFullHeaders) + client.DefaultRequestHeaders.TryAddWithoutValidation(h.Key, h.Value); + + var postParams = new Dictionary + { + { "username", jackett.Toloka.login.u }, + { "password", jackett.Toloka.login.p }, + { "autologin", "on" }, + { "ssl", "on" }, + { "redirect", "index.php?" }, + { "login", "Вхід" } + }; + + using (var postContent = new System.Net.Http.FormUrlEncodedContent(postParams)) + { + using (var response = await client.PostAsync($"{jackett.Toloka.host}/login.php", postContent)) + { + if (response.Headers.TryGetValues("Set-Cookie", out var cook)) + { + string toloka_sid = null, toloka_data = null; + foreach (string line in cook) + { + if (string.IsNullOrWhiteSpace(line)) + continue; + + if (line.Contains("toloka_sid=")) + toloka_sid = new Regex("toloka_sid=([^;]+)(;|$)").Match(line).Groups[1].Value; + + if (line.Contains("toloka_data=")) + toloka_data = new Regex("toloka_data=([^;]+)(;|$)").Match(line).Groups[1].Value; + } + + if (!string.IsNullOrWhiteSpace(toloka_sid) && !string.IsNullOrWhiteSpace(toloka_data)) + { + string cookie = $"toloka_sid={toloka_sid}; toloka_ssl=1; toloka_data={toloka_data};"; + Startup.memoryCache.Set(authKey, cookie, DateTime.Today.AddDays(1)); + return cookie; + } + } + } + } + } + } + } + catch { } + + return null; + } + #endregion + } +} diff --git a/JacRed/Controllers/TorrentByController.cs b/JacRed/Controllers/TorrentByController.cs new file mode 100644 index 0000000..18c57e3 --- /dev/null +++ b/JacRed/Controllers/TorrentByController.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Mvc; + +namespace JacRed.Controllers +{ + [Route("torrentby/[action]")] + public class TorrentByController : JacBaseController + { + #region search + public static Task search(string host, ConcurrentBag torrents, string query, string cat) + { + if (!jackett.TorrentBy.enable || jackett.TorrentBy.showdown) + return Task.FromResult(false); + + return Joinparse(torrents, () => parsePage(host, query, cat)); + } + #endregion + + + #region parseMagnet + async public Task parseMagnet(int id, string magnet) + { + if (!jackett.TorrentBy.enable || jackett.TorrentBy.priority != "torrent") + return Content("disable"); + + var proxyManager = new ProxyManager("torrentby", jackett.TorrentBy); + + var _t = await Http.Download($"{jackett.TorrentBy.host}/d.php?id={id}", referer: jackett.TorrentBy.host, proxy: proxyManager.Get()); + if (_t != null && BencodeTo.Magnet(_t) != null) + return File(_t, "application/x-bittorrent"); + + proxyManager.Refresh(); + + if (string.IsNullOrEmpty(magnet)) + return Content("empty"); + + return Redirect(magnet); + } + #endregion + + #region parsePage + async static ValueTask> parsePage(string host, string query, string cat) + { + #region html + var proxyManager = new ProxyManager("torrentby", jackett.TorrentBy); + + string html = await Http.Get($"{jackett.TorrentBy.host}/search/?search={HttpUtility.UrlEncode(query)}&category={cat}", proxy: proxyManager.Get(), timeoutSeconds: jackett.timeoutSeconds); + + if (html == null || !html.Contains("id=\"find\"")) + { + consoleErrorLog("torrentby"); + proxyManager.Refresh(); + return null; + } + #endregion + + var doc = new HtmlDocument(); + doc.LoadHtml(html.Replace(" ", " ")); + + var nodes = doc.DocumentNode.SelectNodes("//tr[contains(@class, 'ttable_col')]"); + if (nodes == null || nodes.Count == 0) + return null; + + var torrents = new List(); + + foreach (var row in nodes) + { + var hc = new HtmlCommon(row); + + #region Дата создания + DateTime createTime = default; + + if (row.InnerHtml.Contains("Сегодня")) + { + createTime = DateTime.Today; + } + else if (row.InnerHtml.Contains("Вчера")) + { + createTime = DateTime.Today.AddDays(-1); + } + else + { + string _createTime = hc.Match(">([0-9]{4}-[0-9]{2}-[0-9]{2})").Replace("-", " "); + DateTime.TryParseExact(_createTime, "yyyy MM dd", new CultureInfo("ru-RU"), DateTimeStyles.None, out createTime); + } + #endregion + + string url = hc.NodeValue(".//a[@name='search_select']", "href"); + string viewtopic = Regex.Match(url, "^/([0-9]+)").Groups[1].Value; + + string title = hc.NodeValue(".//a[@name='search_select']"); + title = Regex.Replace(title, "<[^>]+>", ""); + + string magnet = hc.Match("href=\"(magnet:\\?xt=[^\"]+)\""); + + if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(magnet)) + continue; + + torrents.Add(new TorrentDetails() + { + url = $"{jackett.TorrentBy.host}/{url.Remove(0, 1)}", + title = title, + sid = HtmlCommon.Integer(hc.NodeValue(".//font[@color='green']")), + pir = HtmlCommon.Integer(hc.NodeValue(".//font[@color='red']")), + sizeName = hc.NodeValue(".//td[contains(text(), 'GB') or contains(text(), 'MB')]"), + magnet = jackett.TorrentBy.priority == "torrent" ? null : magnet, + parselink = jackett.TorrentBy.priority == "torrent" ? $"{host}/torrentby/parsemagnet?id={viewtopic}&magnet={HttpUtility.UrlEncode(magnet)}" : null, + createTime = createTime + }); + } + + return torrents; + } + #endregion + } +} diff --git a/JacRed/Engine/FileDB/FileDB.cs b/JacRed/Engine/FileDB/FileDB.cs new file mode 100644 index 0000000..ec3698c --- /dev/null +++ b/JacRed/Engine/FileDB/FileDB.cs @@ -0,0 +1,39 @@ +using Jackett; +using JacRed.Engine.CORE; +using JacRed.Models; + +namespace JacRed.Engine +{ + public partial class FileDB : IDisposable + { + string fdbkey; + + public ConcurrentDictionary Database = new ConcurrentDictionary(); + + FileDB(string key, bool empty = false) + { + fdbkey = key; + string fdbpath = pathDb(key); + + if (!empty && File.Exists(fdbpath)) + Database = JsonStream.Read>(fdbpath) ?? new ConcurrentDictionary(); + } + + + public void Dispose() + { + if (Database.Count > 0) + JsonStream.Write(pathDb(fdbkey), Database); + + if (openWriteTask.TryGetValue(fdbkey, out WriteTaskModel val)) + { + val.openconnection -= 1; + if (0 >= val.openconnection) + { + if (!ModInit.conf.Red.evercache.enable || (ModInit.conf.Red.evercache.enable && ModInit.conf.Red.evercache.validHour > 0)) + openWriteTask.TryRemove(fdbkey, out _); + } + } + } + } +} diff --git a/JacRed/Engine/FileDB/staticDB.cs b/JacRed/Engine/FileDB/staticDB.cs new file mode 100644 index 0000000..b9f16de --- /dev/null +++ b/JacRed/Engine/FileDB/staticDB.cs @@ -0,0 +1,146 @@ +using Jackett; +using JacRed.Engine.CORE; +using JacRed.Models; + +namespace JacRed.Engine +{ + public partial class FileDB : IDisposable + { + #region FileDB + /// + /// $"{search_name}:{search_originalname}" + /// Верхнее время изменения + /// + public static ConcurrentDictionary masterDb = new ConcurrentDictionary(); + + static ConcurrentDictionary openWriteTask = new ConcurrentDictionary(); + + static FileDB() + { + if (File.Exists("cache/jacred/masterDb.bz")) + masterDb = JsonStream.Read>("cache/jacred/masterDb.bz"); + + if (masterDb == null) + { + if (File.Exists($"cache/jacred/masterDb_{DateTime.Today:dd-MM-yyyy}.bz")) + masterDb = JsonStream.Read>($"cache/jacred/masterDb_{DateTime.Today:dd-MM-yyyy}.bz"); + + if (masterDb == null && File.Exists($"cache/jacred/masterDb_{DateTime.Today.AddDays(-1):dd-MM-yyyy}.bz")) + masterDb = JsonStream.Read>($"cache/jacred/masterDb_{DateTime.Today.AddDays(-1):dd-MM-yyyy}.bz"); + + if (masterDb == null) + masterDb = new ConcurrentDictionary(); + + if (File.Exists("cache/jacred/lastsync.txt")) + File.Delete("cache/jacred/lastsync.txt"); + } + } + #endregion + + #region pathDb + static string pathDb(string key) + { + string md5key = CrypTo.md5(key); + + Directory.CreateDirectory($"cache/jacred/fdb/{md5key.Substring(0, 2)}"); + return $"cache/jacred/fdb/{md5key.Substring(0, 2)}/{md5key.Substring(2)}"; + } + #endregion + + #region Open + public static FileDB Open(string key, bool empty = false) + { + if (empty) + { + openWriteTask.TryRemove(key, out _); + return new FileDB(key, empty: empty); + } + + if (openWriteTask.TryGetValue(key, out WriteTaskModel val)) + { + val.countread++; + val.openconnection += 1; + val.lastread = DateTime.UtcNow; + return val.db; + } + else + { + var fdb = new FileDB(key); + openWriteTask.TryAdd(key, new WriteTaskModel() { db = fdb, openconnection = 1, countread = 1, lastread = DateTime.UtcNow }); + return fdb; + } + } + #endregion + + #region SaveChangesToFile + public static void SaveChangesToFile() + { + try + { + JsonStream.Write("cache/jacred/masterDb.bz", masterDb); + + if (!File.Exists($"cache/jacred/masterDb_{DateTime.Today:dd-MM-yyyy}.bz")) + File.Copy("cache/jacred/masterDb.bz", $"cache/jacred/masterDb_{DateTime.Today:dd-MM-yyyy}.bz"); + + if (File.Exists($"cache/jacred/masterDb_{DateTime.Today.AddDays(-2):dd-MM-yyyy}.bz")) + File.Delete($"cache/jacred/masterDb_{DateTime.Today.AddDays(-2):dd-MM-yyyy}.bz"); + } + catch { } + } + #endregion + + + #region Cron + async public static Task Cron() + { + while (true) + { + await Task.Delay(TimeSpan.FromMinutes(10)); + + if (!ModInit.conf.Red.evercache.enable || 0 >= ModInit.conf.Red.evercache.validHour) + continue; + + try + { + var deleteKeys = openWriteTask + .Where(i => DateTime.UtcNow > i.Value.lastread.AddHours(ModInit.conf.Red.evercache.validHour)) + .Select(i => i.Key) + .ToArray(); + + foreach (string key in deleteKeys) + openWriteTask.TryRemove(key, out _); + } + catch { } + } + } + + async public static Task CronFast() + { + while (true) + { + await Task.Delay(TimeSpan.FromSeconds(20)); + + if (!ModInit.conf.Red.evercache.enable || 0 >= ModInit.conf.Red.evercache.validHour) + continue; + + try + { + if (openWriteTask.Count > ModInit.conf.Red.evercache.maxOpenWriteTask) + { + var deleteKeys = openWriteTask + .Where(i => DateTime.Now > i.Value.create.AddMinutes(10)) + .OrderBy(i => i.Value.countread).ThenBy(i => i.Value.lastread) + .Take(ModInit.conf.Red.evercache.dropCacheTake) + .Select(i => i.Key) + .ToArray(); + + foreach (string key in deleteKeys) + openWriteTask.TryRemove(key, out _); + } + } + catch { } + } + } + #endregion + } +} diff --git a/JacRed/Engine/JacBaseController.cs b/JacRed/Engine/JacBaseController.cs new file mode 100644 index 0000000..416e922 --- /dev/null +++ b/JacRed/Engine/JacBaseController.cs @@ -0,0 +1,33 @@ +using Jackett; +using JacRed.Models.AppConf; + +namespace JacRed.Engine +{ + public class JacBaseController : BaseController + { + public static RedConf red => ModInit.conf.Red; + + public static JacConf jackett => ModInit.conf.Jackett; + + + async public static Task Joinparse(ConcurrentBag torrents, Func>> parse) + { + var result = await parse(); + + if (result != null && result.Count > 0) + { + foreach (TorrentDetails torrent in result) + torrents.Add(torrent); + + return true; + } + + return false; + } + + public static void consoleErrorLog(string plugin) + { + Console.WriteLine($"JacRed: InternalServerError - {plugin}"); + } + } +} diff --git a/JacRed/Engine/JackettApi.cs b/JacRed/Engine/JackettApi.cs new file mode 100644 index 0000000..9d87cdb --- /dev/null +++ b/JacRed/Engine/JackettApi.cs @@ -0,0 +1,286 @@ +using Jackett; +using JacRed.Controllers; +using JacRed.Models.AppConf; +using System.Reflection; + +namespace JacRed.Engine +{ + public static class JackettApi + { + static JacConf jackett => ModInit.conf.Jackett; + + #region Indexers + async public static Task> Indexers(string host, string query, string title, string title_original, int year, int is_serial, Dictionary category) + { + var hybridCache = IHybridCache.Get(null); + + string mkey = $"JackettApi:{query}:{title}:{year}:{is_serial}"; + if (hybridCache.TryGetValue(mkey, out List cache, inmemory: false)) + return cache; + + var torrents = new ConcurrentBag(); + + #region search + string search = jackett.search_lang == "query" ? query : jackett.search_lang == "title" ? title : title_original; + + if (string.IsNullOrWhiteSpace(search)) + { + search = query ?? title ?? title_original; + if (string.IsNullOrWhiteSpace(search)) + return torrents.ToList(); + } + #endregion + + #region category + if (category != null) + { + string cat = category.FirstOrDefault().Value; + if (cat != null) + { + if (cat.Contains("5020") || cat.Contains("2010")) + is_serial = 3; // tvshow + else if (cat.Contains("5080")) + is_serial = 4; // док + else if (cat.Contains("5070")) + is_serial = 5; // аниме + else if (is_serial == 0) + { + if (cat.StartsWith("20")) + is_serial = 1; // фильм + else if (cat.StartsWith("50")) + is_serial = 2; // сериал + } + } + } + #endregion + + #region modpars + void modpars(List tasks, string cat) + { + if (AppInit.modules != null && AppInit.modules.Count > 0) + { + foreach (var item in AppInit.modules) + { + foreach (var mod in item.jac) + { + if (mod.enable) + { + try + { + if (item.assembly.GetType(mod.@namespace) is Type t && t.GetMethod("parsePage") is MethodInfo m) + { + var task = (Task)m.Invoke(null, new object[] { host, torrents, search, cat }); + if (task != null) + tasks.Add(task); + } + } + catch { } + } + } + } + } + } + #endregion + + #region Парсим торренты + if (is_serial == 1) + { + #region Фильм + var tasks = new List + { + RutorController.search(host, torrents, search, "1"), // movie + RutorController.search(host, torrents, search, "5"), // movie + RutorController.search(host, torrents, search, "7"), // multfilm + RutorController.search(host, torrents, search, "12"), // documovie + RutorController.search(host, torrents, search, "17", true, "1"), // UKR + + MegapeerController.search(host, torrents, search, "79"), // Наши фильмы + MegapeerController.search(host, torrents, search, "80"), // Зарубежные фильмы + MegapeerController.search(host, torrents, search, "76"), // Мультипликация + + TorrentByController.search(host, torrents, search, "1"), // movie + TorrentByController.search(host, torrents, search, "2"), // movie + TorrentByController.search(host, torrents, search, "5"), // multfilm + + KinozalController.search(host, torrents, search, new string[] { "movie", "multfilm", "tvshow" }), + NNMClubController.search(host, torrents, search, new string[] { "movie", "multfilm", "documovie" }), + TolokaController.search(host, torrents, search, new string[] { "movie", "multfilm", "documovie" }), + RutrackerController.search(host, torrents, search, new string[] { "movie", "multfilm", "documovie" }), + BitruController.search(host, torrents, search, new string[] { "movie" }), + SelezenController.search(host, torrents, search), + BigFanGroup.search(host, torrents, search, new string[] { "movie", "multfilm", "documovie" }) + }; + + modpars(tasks, "movie"); + + await Task.WhenAll(tasks); + #endregion + } + else if (is_serial == 2) + { + #region Сериал + var tasks = new List + { + RutorController.search(host, torrents, search, "4"), // serial + RutorController.search(host, torrents, search, "16"), // serial + RutorController.search(host, torrents, search, "7"), // multserial + RutorController.search(host, torrents, search, "12"), // docuserial + RutorController.search(host, torrents, search, "6"), // tvshow + RutorController.search(host, torrents, search, "17", true, "4"), // UKR + + MegapeerController.search(host, torrents, search, "5"), // serial + MegapeerController.search(host, torrents, search, "6"), // serial + MegapeerController.search(host, torrents, search, "55"), // docuserial + MegapeerController.search(host, torrents, search, "57"), // tvshow + MegapeerController.search(host, torrents, search, "76"), // multserial + + TorrentByController.search(host, torrents, search, "3"), // serial + TorrentByController.search(host, torrents, search, "5"), // multserial + TorrentByController.search(host, torrents, search, "4"), // tvshow + TorrentByController.search(host, torrents, search, "12"), // tvshow + + KinozalController.search(host, torrents, search, new string[] { "serial", "multserial", "tvshow" }), + NNMClubController.search(host, torrents, search, new string[] { "serial", "multserial", "docuserial" }), + TolokaController.search(host, torrents, search, new string[] { "serial", "multserial", "docuserial" }), + RutrackerController.search(host, torrents, search, new string[] { "serial", "multserial", "docuserial" }), + BitruController.search(host, torrents, search, new string[] { "serial" }), + LostfilmController.search(host, torrents, search), + BigFanGroup.search(host, torrents, search, new string[] { "serial", "multserial", "docuserial", "tvshow" }) + }; + + modpars(tasks, "serial"); + + await Task.WhenAll(tasks); + #endregion + } + else if (is_serial == 3) + { + #region tvshow + var tasks = new List + { + RutorController.search(host, torrents, search, "6"), + MegapeerController.search(host, torrents, search, "57"), + TorrentByController.search(host, torrents, search, "4"), + TorrentByController.search(host, torrents, search, "12"), + KinozalController.search(host, torrents, search, new string[] { "tvshow" }), + NNMClubController.search(host, torrents, search, new string[] { "docuserial", "documovie" }), + TolokaController.search(host, torrents, search, new string[] { "docuserial", "documovie" }), + RutrackerController.search(host, torrents, search, new string[] { "tvshow" }), + BigFanGroup.search(host, torrents, search, new string[] { "tvshow" }) + }; + + modpars(tasks, "tvshow"); + + await Task.WhenAll(tasks); + #endregion + } + else if (is_serial == 4) + { + #region docuserial / documovie + var tasks = new List + { + RutorController.search(host, torrents, search, "12"), + MegapeerController.search(host, torrents, search, "55"), + NNMClubController.search(host, torrents, search, new string[] { "docuserial", "documovie" }), + TolokaController.search(host, torrents, search, new string[] { "docuserial", "documovie" }), + RutrackerController.search(host, torrents, search, new string[] { "docuserial", "documovie" }), + BigFanGroup.search(host, torrents, search, new string[] { "docuserial", "documovie" }) + }; + + modpars(tasks, "documental"); + + await Task.WhenAll(tasks); + #endregion + } + else if (is_serial == 5) + { + #region anime + string animesearch = title ?? query; + + var tasks = new List + { + RutorController.search(host, torrents, animesearch, "10"), + TorrentByController.search(host, torrents, animesearch, "6"), + KinozalController.search(host, torrents, animesearch, new string[] { "anime" }), + NNMClubController.search(host, torrents, animesearch, new string[] { "anime" }), + RutrackerController.search(host, torrents, animesearch, new string[] { "anime" }), + TolokaController.search(host, torrents, search, new string[] { "anime" }), + AniLibriaController.search(host, torrents, animesearch), + AnimeLayerController.search(host, torrents, animesearch), + AnifilmController.search(host, torrents, animesearch) + }; + + modpars(tasks, "anime"); + + await Task.WhenAll(tasks); + #endregion + } + else + { + #region Неизвестно + var tasks = new List + { + RutorController.search(host, torrents, search, "0"), + MegapeerController.search(host, torrents, search, "0"), + TorrentByController.search(host, torrents, search, "0"), + KinozalController.search(host, torrents, search, null), + NNMClubController.search(host, torrents, search, null), + BitruController.search(host, torrents, search, null), + RutrackerController.search(host, torrents, search, null), + TolokaController.search(host, torrents, search, null), + AniLibriaController.search(host, torrents, search), + AnimeLayerController.search(host, torrents, search), + AnifilmController.search(host, torrents, search), + SelezenController.search(host, torrents, search), + LostfilmController.search(host, torrents, search), + BigFanGroup.search(host, torrents, search, null) + }; + + modpars(tasks, "search"); + + await Task.WhenAll(tasks); + #endregion + } + #endregion + + var hash = new HashSet(); + var finaly = new List(torrents.Count); + + foreach (var t in torrents) + { + if (t.trackerName == null) + t.trackerName = Regex.Match(t.url, "https?://([^/]+)").Groups[1].Value; + + if (!string.IsNullOrEmpty(ModInit.conf.filter) && !Regex.IsMatch(t.title, ModInit.conf.filter, RegexOptions.IgnoreCase)) + continue; + + if (!string.IsNullOrEmpty(ModInit.conf.filter_ignore) && Regex.IsMatch(t.title, ModInit.conf.filter_ignore, RegexOptions.IgnoreCase)) + continue; + + if (!hash.Contains(t.url)) + { + hash.Add(t.url); + finaly.Add(t); + } + } + + var result = finaly.AsEnumerable(); + + if (is_serial == 1 && year > 0) + result = result.Where(i => i.title.Contains(year.ToString()) || i.title.Contains($"{year+1}") || i.title.Contains($"{year-1}")); + + if (ModInit.conf.Jackett.cacheToMinutes > 0) + hybridCache.Set(mkey, result.ToList(), DateTime.Now.AddMinutes(ModInit.conf.Jackett.cacheToMinutes), inmemory: false); + + return result.ToList(); + } + #endregion + + #region Api + public static Task> Api(string host, string search) + { + return Indexers(host, search, null, null, 0, 0, null); + } + #endregion + } +} diff --git a/JacRed/Engine/JsonStream.cs b/JacRed/Engine/JsonStream.cs new file mode 100644 index 0000000..6a67509 --- /dev/null +++ b/JacRed/Engine/JsonStream.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using System.IO.Compression; + +namespace JacRed.Engine.CORE +{ + public static class JsonStream + { + #region Read + public static T Read(string path) + { + try + { + var settings = new JsonSerializerSettings + { + Error = (se, ev) => { ev.ErrorContext.Handled = true; } + }; + + var serializer = JsonSerializer.Create(settings); + + using (Stream file = new GZipStream(File.OpenRead(path), CompressionMode.Decompress)) + { + using (var sr = new StreamReader(file)) + { + using (var jsonTextReader = new JsonTextReader(sr)) + { + return serializer.Deserialize(jsonTextReader); + } + } + } + } + catch { return default; } + } + #endregion + + #region Write + public static void Write(string path, object db) + { + try + { + //var settings = new JsonSerializerSettings() + //{ + // Formatting = Formatting.Indented + //}; + + var serializer = JsonSerializer.Create(); // settings + + using (var sw = new StreamWriter(new GZipStream(File.OpenWrite(path), CompressionMode.Compress))) + { + using (var jsonTextWriter = new JsonTextWriter(sw)) + { + serializer.Serialize(jsonTextWriter, db); + } + } + } + catch { } + } + #endregion + } +} diff --git a/JacRed/Engine/RedApi.cs b/JacRed/Engine/RedApi.cs new file mode 100644 index 0000000..967afdf --- /dev/null +++ b/JacRed/Engine/RedApi.cs @@ -0,0 +1,569 @@ +using MonoTorrent; +using JacRed.Models.AppConf; +using Jackett; + +namespace JacRed.Engine +{ + public static class RedApi + { + static RedConf red => ModInit.conf.Red; + + #region Indexers + public static (IEnumerable torrents, bool setcache) Indexers(bool rqnum, string apikey, string query, string title, string title_original, int year, int is_serial, Dictionary category) + { + bool setcache = false; + var torrents = new Dictionary(); + + #region category + if (is_serial == 0 && category != null) + { + string cat = category.FirstOrDefault().Value; + if (cat != null) + { + if (cat.Contains("5020") || cat.Contains("2010")) + is_serial = 3; // tvshow + else if (cat.Contains("5080")) + is_serial = 4; // док + else if (cat.Contains("5070")) + is_serial = 5; // аниме + else if (is_serial == 0) + { + if (cat.StartsWith("20")) + is_serial = 1; // фильм + else if (cat.StartsWith("50")) + is_serial = 2; // сериал + } + } + } + #endregion + + #region AddTorrents + void AddTorrents(TorrentDetails t) + { + if (t.url == null) + return; + + if (!string.IsNullOrEmpty(ModInit.conf.filter) && !Regex.IsMatch(t.title, ModInit.conf.filter, RegexOptions.IgnoreCase)) + return; + + if (!string.IsNullOrEmpty(ModInit.conf.filter_ignore) && Regex.IsMatch(t.title, ModInit.conf.filter_ignore, RegexOptions.IgnoreCase)) + return; + + if (InvkEvent.conf.RedApi?.AddTorrents != null) + { + if (!InvkEvent.RedApi("addtorrent", t)) + return; + } + else + { + EventListener.RedApiAddTorrents?.Invoke(t); + } + + if (torrents.TryGetValue(t.url, out TorrentDetails val)) + { + if (t.updateTime > val.updateTime) + torrents[t.url] = t; + } + else + { + torrents.TryAdd(t.url, t); + } + } + #endregion + + if (!string.IsNullOrWhiteSpace(title) || !string.IsNullOrWhiteSpace(title_original)) + { + #region Точный поиск + setcache = true; + + string _n = StringConvert.SearchName(title); + string _o = StringConvert.SearchName(title_original); + + // Быстрая выборка по совпадению ключа в имени + var mdb = FileDB.masterDb.Where(i => _n != null && i.Key.StartsWith($"{_n}:") || _o != null && i.Key.EndsWith($":{_o}")); + if (!red.evercache.enable || red.evercache.validHour > 0) + mdb = mdb.Take(red.maxreadfile); + + foreach (var val in mdb) + { + using (var fdb = FileDB.Open(val.Key)) + { + foreach (var t in fdb.Database.Values) + { + if (t.types == null || t.title.Contains(" КПК")) + continue; + + string name = StringConvert.SearchName(t.name); + string originalname = StringConvert.SearchName(t.originalname); + + // Точная выборка по name или originalname + if (_n != null && _n == name || _o != null && _o == originalname) + { + if (is_serial == 1) + { + #region Фильм + if (t.types.Contains("movie") || t.types.Contains("multfilm") || t.types.Contains("anime") || t.types.Contains("documovie")) + { + if (Regex.IsMatch(t.title, " (сезон|сери(и|я|й))", RegexOptions.IgnoreCase)) + continue; + + if (year > 0) + { + if (t.relased == year || t.relased == year - 1 || t.relased == year + 1) + AddTorrents(t); + } + else + { + AddTorrents(t); + } + } + #endregion + } + else if (is_serial == 2) + { + #region Сериал + if (t.types.Contains("serial") || t.types.Contains("multserial") || t.types.Contains("anime") || t.types.Contains("docuserial") || t.types.Contains("tvshow")) + { + if (year > 0) + { + if (t.relased >= year - 1) + AddTorrents(t); + } + else + { + AddTorrents(t); + } + } + #endregion + } + else if (is_serial == 3) + { + #region tvshow + if (t.types.Contains("tvshow")) + { + if (year > 0) + { + if (t.relased >= year - 1) + AddTorrents(t); + } + else + { + AddTorrents(t); + } + } + #endregion + } + else if (is_serial == 4) + { + #region docuserial / documovie + if (t.types.Contains("docuserial") || t.types.Contains("documovie")) + { + if (year > 0) + { + if (t.relased >= year - 1) + AddTorrents(t); + } + else + { + AddTorrents(t); + } + } + #endregion + } + else if (is_serial == 5) + { + #region anime + if (t.types.Contains("anime")) + { + if (year > 0) + { + if (t.relased >= year - 1) + AddTorrents(t); + } + else + { + AddTorrents(t); + } + } + #endregion + } + else + { + #region Неизвестно + if (year > 0) + { + if (t.types.Contains("movie") || t.types.Contains("multfilm") || t.types.Contains("documovie")) + { + if (t.relased == year || t.relased == year - 1 || t.relased == year + 1) + AddTorrents(t); + } + else + { + if (t.relased >= year - 1) + AddTorrents(t); + } + } + else + { + AddTorrents(t); + } + #endregion + } + } + } + } + } + #endregion + } + else if (!string.IsNullOrWhiteSpace(query) && query.Length > 1) + { + #region Обычный поиск + string _s = StringConvert.SearchName(query); + + #region torrentsSearch + void torrentsSearch(bool exact) + { + var mdb = FileDB.masterDb.Where(i => i.Key.Contains(_s)); + if (!red.evercache.enable || red.evercache.validHour > 0) + mdb = mdb.Take(red.maxreadfile); + + foreach (var val in mdb) + { + using (var fdb = FileDB.Open(val.Key)) + { + foreach (var t in fdb.Database.Values) + { + if (exact) + { + if (StringConvert.SearchName(t.name) != _s && StringConvert.SearchName(t.originalname) != _s) + continue; + } + + if (t.types == null || t.title.Contains(" КПК")) + continue; + + if (is_serial == 1) + { + if (t.types.Contains("movie") || t.types.Contains("multfilm") || t.types.Contains("anime") || t.types.Contains("documovie")) + AddTorrents(t); + } + else if (is_serial == 2) + { + if (t.types.Contains("serial") || t.types.Contains("multserial") || t.types.Contains("anime") || t.types.Contains("docuserial") || t.types.Contains("tvshow")) + AddTorrents(t); + } + else if (is_serial == 3) + { + if (t.types.Contains("tvshow")) + AddTorrents(t); + } + else if (is_serial == 4) + { + if (t.types.Contains("docuserial") || t.types.Contains("documovie")) + AddTorrents(t); + } + else if (is_serial == 5) + { + if (t.types.Contains("anime")) + AddTorrents(t); + } + else + { + AddTorrents(t); + } + } + } + + } + } + #endregion + + if (is_serial == -1) + torrentsSearch(exact: false); + else + { + torrentsSearch(exact: true); + if (torrents.Count == 0) + torrentsSearch(exact: false); + } + #endregion + } + + #region Объединить дубликаты + IEnumerable tsort = null; + + if (ModInit.conf.typesearch == "red" && ((!rqnum && red.mergeduplicates) || (rqnum && red.mergenumduplicates))) + { + var temp = new Dictionary AnnounceUrls)>(); + + foreach (var torrent in torrents.Values + .Where(i => red.trackers == null || red.trackers.Contains(i.trackerName)) + .OrderByDescending(i => i.createTime) + .ThenBy(i => i.trackerName == "selezen").ToList()) + { + if (torrent.magnet == null) + continue; + + var magnetLink = MagnetLink.Parse(torrent.magnet); + string hex = magnetLink.InfoHashes.V1.ToHex(); + + if (!temp.TryGetValue(hex, out _)) + { + temp.TryAdd(hex, ((TorrentDetails)torrent.Clone(), torrent.trackerName == "kinozal" ? torrent.title : null, magnetLink.Name, magnetLink.AnnounceUrls?.ToList() ?? new List())); + } + else + { + var t = temp[hex]; + t.torrent.trackerName += $", {torrent.trackerName}"; + + #region urls + if (t.torrent.urls == null) + t.torrent.urls = new HashSet { t.torrent.url }; + + t.torrent.urls.Add(torrent.url); + #endregion + + #region UpdateMagnet + void UpdateMagnet() + { + string magnet = $"magnet:?xt=urn:btih:{hex.ToLower()}"; + + if (!string.IsNullOrWhiteSpace(t.Name)) + magnet += $"&dn={HttpUtility.UrlEncode(t.Name)}"; + + if (t.AnnounceUrls != null && t.AnnounceUrls.Count > 0) + { + foreach (string announce in t.AnnounceUrls) + { + string tr = announce.Contains("/") || announce.Contains(":") ? HttpUtility.UrlEncode(announce) : announce; + + if (!magnet.Contains(tr)) + magnet += $"&tr={tr}"; + } + } + + t.torrent.magnet = magnet; + } + #endregion + + if (string.IsNullOrWhiteSpace(t.Name) && !string.IsNullOrWhiteSpace(magnetLink.Name)) + { + t.Name = magnetLink.Name; + temp[hex] = t; + UpdateMagnet(); + } + + if (magnetLink.AnnounceUrls != null && magnetLink.AnnounceUrls.Count > 0) + { + t.AnnounceUrls.AddRange(magnetLink.AnnounceUrls); + UpdateMagnet(); + } + + #region UpdateTitle + void UpdateTitle() + { + if (string.IsNullOrWhiteSpace(t.title)) + return; + + string title = t.title; + + if (t.torrent.voices != null && t.torrent.voices.Count > 0) + title += $" | {string.Join(" | ", t.torrent.voices)}"; + + t.torrent.title = title; + } + + if (torrent.trackerName == "kinozal") + { + t.title = torrent.title; + temp[hex] = t; + UpdateTitle(); + } + + if (torrent.voices != null && torrent.voices.Count > 0) + { + if (t.torrent.voices == null) + { + t.torrent.voices = torrent.voices; + } + else + { + foreach (var v in torrent.voices) + t.torrent.voices.Add(v); + } + + UpdateTitle(); + } + #endregion + + if (torrent.trackerName != "selezen") + { + if (torrent.sid > t.torrent.sid) + t.torrent.sid = torrent.sid; + + if (torrent.pir > t.torrent.pir) + t.torrent.pir = torrent.pir; + } + + if (torrent.createTime > t.torrent.createTime) + t.torrent.createTime = torrent.createTime; + + if (torrent.voices != null && torrent.voices.Count > 0) + { + if (t.torrent.voices == null) + t.torrent.voices = new HashSet(); + + foreach (var v in torrent.voices) + t.torrent.voices.Add(v); + } + + if (torrent.languages != null && torrent.languages.Count > 0) + { + if (t.torrent.languages == null) + t.torrent.languages = new HashSet(); + + foreach (var v in torrent.languages) + t.torrent.languages.Add(v); + } + + if (t.torrent.ffprobe == null) + t.torrent.ffprobe = torrent.ffprobe; + } + } + + tsort = temp.Select(i => i.Value.torrent); + } + else + { + tsort = torrents.Values.Where(i => red.trackers == null || red.trackers.Contains(i.trackerName)); + } + #endregion + + if (apikey == "rus") + return (tsort.Where(i => i.languages != null && i.languages.Contains("rus") || i.types != null && (i.types.Contains("sport") || i.types.Contains("tvshow") || i.types.Contains("docuserial"))), setcache); + + return (tsort, setcache); + } + #endregion + + #region Api + public static IEnumerable Api(string search, string altname, bool exact, string type, string sort, string tracker, string voice, string videotype, long relased, long quality, long season) + { + var torrents = new Dictionary(); + + #region AddTorrents + void AddTorrents(TorrentDetails t) + { + if (torrents.TryGetValue(t.url, out TorrentDetails val)) + { + if (t.updateTime > val.updateTime) + torrents[t.url] = t; + } + else + { + torrents.TryAdd(t.url, t); + } + } + #endregion + + if (string.IsNullOrWhiteSpace(search) || search.Length == 1) + return new List(); + + string _s = StringConvert.SearchName(search); + string _altsearch = StringConvert.SearchName(altname); + + if (exact) + { + #region Точный поиск + foreach (var mdb in FileDB.masterDb.Where(i => i.Key.StartsWith($"{_s}:") || i.Key.EndsWith($":{_s}") || _altsearch != null && i.Key.Contains(_altsearch))) + { + using (var fdb = FileDB.Open(mdb.Key)) + { + foreach (var t in fdb.Database.Values) + { + if (t.types == null) + continue; + + if (string.IsNullOrWhiteSpace(type) || t.types.Contains(type)) + { + string _n = StringConvert.SearchName(t.name); + string _o = StringConvert.SearchName(t.originalname); + + if (_n == _s || _o == _s || _altsearch != null && (_n == _altsearch || _o == _altsearch)) + AddTorrents(t); + } + } + } + } + #endregion + } + else + { + #region Поиск по совпадению ключа в имени + var mdb = FileDB.masterDb.Where(i => i.Key.Contains(_s) || _altsearch != null && i.Key.Contains(_altsearch)); + if (!red.evercache.enable || red.evercache.validHour > 0) + mdb = mdb.Take(red.maxreadfile); + + foreach (var val in mdb) + { + using (var fdb = FileDB.Open(val.Key)) + { + foreach (var t in fdb.Database.Values) + { + if (t.types == null) + continue; + + if (string.IsNullOrWhiteSpace(type) || t.types.Contains(type)) + AddTorrents(t); + } + } + } + #endregion + } + + if (torrents.Count == 0) + return new List(); + + IEnumerable query = torrents.Values; + + #region sort + switch (sort ?? string.Empty) + { + case "sid": + query = query.OrderByDescending(i => i.sid); + break; + case "pir": + query = query.OrderByDescending(i => i.pir); + break; + case "size": + query = query.OrderByDescending(i => i.size); + break; + default: + query = query.OrderByDescending(i => i.createTime); + break; + } + #endregion + + if (!string.IsNullOrWhiteSpace(tracker)) + query = query.Where(i => i.trackerName == tracker); + + if (relased > 0) + query = query.Where(i => i.relased == relased); + + if (quality > 0) + query = query.Where(i => i.quality == quality); + + if (!string.IsNullOrWhiteSpace(videotype)) + query = query.Where(i => i.videotype == videotype); + + if (!string.IsNullOrWhiteSpace(voice)) + query = query.Where(i => i.voices.Contains(voice)); + + if (season > 0) + query = query.Where(i => i.seasons.Contains((int)season)); + + return query.Where(i => red.trackers == null || red.trackers.Contains(i.trackerName)); + } + #endregion + } +} diff --git a/JacRed/Engine/SyncCron.cs b/JacRed/Engine/SyncCron.cs new file mode 100644 index 0000000..252e2be --- /dev/null +++ b/JacRed/Engine/SyncCron.cs @@ -0,0 +1,117 @@ +using Jackett; +using JacRed.Models.Sync; + +namespace JacRed.Engine +{ + public static class SyncCron + { + static long lastsync = -1; + + async public static Task Run() + { + bool reset = true; + await Task.Delay(TimeSpan.FromMinutes(2)); + + DateTime lastSave = DateTime.Now; + + while (true) + { + try + { + if (File.Exists(@"C:\ProgramData\lampac\disablesync")) + break; + + if (ModInit.conf.typesearch == "red" && !string.IsNullOrWhiteSpace(ModInit.conf.Red.syncapi)) + { + if (lastsync == -1 && File.Exists("cache/jacred/lastsync.txt")) + lastsync = long.Parse(File.ReadAllText("cache/jacred/lastsync.txt")); + + var root = await Http.Get($"{ModInit.conf.Red.syncapi}/sync/fdb/torrents?time={lastsync}", timeoutSeconds: 300, MaxResponseContentBufferSize: 100_000_000, weblog: false); + + if (root?.collections == null) + { + if (reset) + { + reset = false; + await Task.Delay(TimeSpan.FromMinutes(1)); + continue; + } + } + else if (root.collections.Count > 0) + { + reset = true; + foreach (var collection in root.collections) + { + bool updateMasterDb = false; + + using (var fdb = FileDB.Open(collection.Key, empty: true)) + { + foreach (var torrent in collection.Value.torrents) + { + if (torrent.Value.types == null || torrent.Value.types.Contains("sport")) + continue; + + fdb.Database.AddOrUpdate(torrent.Key, torrent.Value, (k, v) => torrent.Value); + updateMasterDb = true; + } + } + + if (updateMasterDb) + { + if (FileDB.masterDb.ContainsKey(collection.Key)) + { + FileDB.masterDb[collection.Key] = collection.Value.time; + } + else + { + FileDB.masterDb.TryAdd(collection.Key, collection.Value.time); + } + } + } + + lastsync = root.collections.Last().Value.fileTime; + + if (root.nextread) + { + if (DateTime.Now > lastSave.AddMinutes(5)) + { + lastSave = DateTime.Now; + FileDB.SaveChangesToFile(); + File.WriteAllText("cache/jacred/lastsync.txt", lastsync.ToString()); + } + + continue; + } + } + + FileDB.SaveChangesToFile(); + File.WriteAllText("cache/jacred/lastsync.txt", lastsync.ToString()); + } + else + { + await Task.Delay(TimeSpan.FromMinutes(1)); + continue; + } + } + catch + { + try + { + if (lastsync > 0) + { + FileDB.SaveChangesToFile(); + File.WriteAllText("cache/jacred/lastsync.txt", lastsync.ToString()); + } + } + catch { } + } + + await Task.Delay(1000 * Random.Shared.Next(60, 300)); + await Task.Delay(1000 * 60 * (20 > ModInit.conf.Red.syntime ? 20 : ModInit.conf.Red.syntime)); + + reset = true; + lastSave = DateTime.Now; + } + } + } +} diff --git a/JacRed/Engine/WebApi.cs b/JacRed/Engine/WebApi.cs new file mode 100644 index 0000000..07b5bbd --- /dev/null +++ b/JacRed/Engine/WebApi.cs @@ -0,0 +1,99 @@ +using Jackett; +using Newtonsoft.Json.Linq; +using System.Text; +using Shared.Models.JacRed.Tracks; + +namespace JacRed.Engine +{ + public static class WebApi + { + #region Indexers + async public static Task> Indexers(string query, string title, string title_original, int year, int is_serial, Dictionary category) + { + var queryString = new StringBuilder(); + + if (!string.IsNullOrEmpty(title)) + queryString.Append($"&title={HttpUtility.UrlEncode(title)}"); + + if (!string.IsNullOrEmpty(title_original)) + queryString.Append($"&title_original={HttpUtility.UrlEncode(title_original)}"); + + if (year > 0) + queryString.Append($"&year={year}"); + + if (is_serial > 0) + queryString.Append($"&is_serial={is_serial}"); + + if (category != null && category.Count > 0) + queryString.Append($"&category[]={category.First().Value}"); + + var root = await Http.Get($"{ModInit.conf.webApiHost}/api/v2.0/indexers/all/results?query={HttpUtility.UrlEncode(query)}" + queryString.ToString(), timeoutSeconds: 8); + if (root == null) + return new List(); + + var results = root.GetValue("Results")?.ToObject(); + if (results == null || results.Count == 0) + return new List(); + + var torrents = new List(results.Count); + + foreach (var torrent in results) + { + try + { + string name = torrent.Value("Title"); + string tracker = torrent.Value("Tracker"); + + if (ModInit.conf.Red.trackers != null) + { + if (!tracker.Contains(",")) + { + if (!ModInit.conf.Red.trackers.Contains(tracker)) + continue; + } + else + { + /* + * Этот код фильтрует результаты поиска торрентов по списку разрешённых трекеров, который хранится в ModInit.conf.Red.trackers. + * Если у торрента в поле Tracker указано несколько трекеров через запятую, то он будет допущен только в том случае, если хотя бы один из этих трекеров есть в разрешённом списке. + */ + var trackers = tracker.Split(','); + if (!ModInit.conf.Red.trackers.Any(t => trackers.Contains(t))) + continue; + } + } + + if (!string.IsNullOrEmpty(ModInit.conf.filter) && !Regex.IsMatch(name, ModInit.conf.filter, RegexOptions.IgnoreCase)) + continue; + + if (!string.IsNullOrEmpty(ModInit.conf.filter_ignore) && Regex.IsMatch(name, ModInit.conf.filter_ignore, RegexOptions.IgnoreCase)) + continue; + + torrents.Add(new TorrentDetails() + { + trackerName = tracker, + url = torrent.Value("Details"), + title = name, + sid = torrent.Value("Seeders"), + pir = torrent.Value("Peers"), + size = torrent.Value("Size"), + magnet = torrent.Value("MagnetUri"), + createTime = torrent.Value("PublishDate"), + ffprobe = torrent["ffprobe"]?.ToObject>() + }); + } + catch { } + } + + return torrents; + } + #endregion + + #region Api + public static Task> Api(string search) + { + return Indexers(search, null, null, 0, 0, null); + } + #endregion + } +} diff --git a/JacRed/GlobalUsings.cs b/JacRed/GlobalUsings.cs new file mode 100644 index 0000000..d76c461 --- /dev/null +++ b/JacRed/GlobalUsings.cs @@ -0,0 +1,16 @@ +global using System; +global using System.Web; +global using System.Threading.Tasks; +global using System.Collections.Generic; +global using System.Text.RegularExpressions; +global using System.Collections.Concurrent; +global using System.Globalization; +global using System.IO; +global using System.Linq; +global using HtmlAgilityPack; +global using Shared; +global using Shared.Models; +global using Shared.Engine; +global using JacRed.Engine; +global using Shared.Engine.JacRed; +global using Shared.Models.JacRed; \ No newline at end of file diff --git a/JacRed/JacRed.csproj b/JacRed/JacRed.csproj new file mode 100644 index 0000000..a644285 --- /dev/null +++ b/JacRed/JacRed.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + library + true + + + + + + + diff --git a/JacRed/ModInit.cs b/JacRed/ModInit.cs new file mode 100644 index 0000000..0423d28 --- /dev/null +++ b/JacRed/ModInit.cs @@ -0,0 +1,117 @@ +using JacRed.Models.AppConf; +using Newtonsoft.Json; +using System.Threading; + +namespace Jackett +{ + public class ModInit + { + #region ModInit + static (ModInit, DateTime) cacheconf = default; + + public static ModInit conf + { + get + { + if (cacheconf.Item1 == null) + { + if (!File.Exists("module/JacRed.conf")) + return new ModInit(); + } + + var lastWriteTime = File.GetLastWriteTime("module/JacRed.conf"); + + if (cacheconf.Item2 != lastWriteTime) + { + var jss = new JsonSerializerSettings { Error = (se, ev) => + { + ev.ErrorContext.Handled = true; + Console.WriteLine("module/JacRed.conf - " + ev.ErrorContext.Error + "\n\n"); + }}; + + string json = File.ReadAllText("module/JacRed.conf"); + if (!json.TrimStart().StartsWith("{")) + json = "{"+json+"}"; + + cacheconf.Item1 = JsonConvert.DeserializeObject(json, jss); + cacheconf.Item2 = lastWriteTime; + } + + return cacheconf.Item1; + } + } + #endregion + + public static void loaded() + { + Directory.CreateDirectory("cache/jacred"); + File.WriteAllText("module/JacRed.current.conf", JsonConvert.SerializeObject(conf, Formatting.Indented)); + + ThreadPool.QueueUserWorkItem(async _ => await SyncCron.Run()); + ThreadPool.QueueUserWorkItem(async _ => await FileDB.Cron()); + ThreadPool.QueueUserWorkItem(async _ => await FileDB.CronFast()); + + + ThreadPool.QueueUserWorkItem(async _ => + { + while (true) + { + await Task.Delay(TimeSpan.FromMinutes(5)); + + try + { + if (conf.typesearch == "jackett" || conf.merge == "jackett") + { + async ValueTask showdown(string name, TrackerSettings settings) + { + if (!settings.monitor_showdown) + return false; + + var proxyManager = new ProxyManager(name, settings); + string html = await Http.Get($"{settings.host}", timeoutSeconds: conf.Jackett.timeoutSeconds, proxy: proxyManager.Get(), weblog: false); + return html == null; + } + + conf.Jackett.Rutor.showdown = await showdown("rutor", conf.Jackett.Rutor); + conf.Jackett.Megapeer.showdown = await showdown("megapeer", conf.Jackett.Megapeer); + conf.Jackett.TorrentBy.showdown = await showdown("torrentby", conf.Jackett.TorrentBy); + conf.Jackett.Kinozal.showdown = await showdown("kinozal", conf.Jackett.Kinozal); + conf.Jackett.NNMClub.showdown = await showdown("nnmclub", conf.Jackett.NNMClub); + conf.Jackett.Bitru.showdown = await showdown("bitru", conf.Jackett.Bitru); + conf.Jackett.Toloka.showdown = await showdown("toloka", conf.Jackett.Toloka); + conf.Jackett.Rutracker.showdown = await showdown("rutracker", conf.Jackett.Rutracker); + conf.Jackett.BigFanGroup.showdown = await showdown("bigfangroup", conf.Jackett.BigFanGroup); + conf.Jackett.Selezen.showdown = await showdown("selezen", conf.Jackett.Selezen); + conf.Jackett.Lostfilm.showdown = await showdown("lostfilm", conf.Jackett.Lostfilm); + conf.Jackett.Anilibria.showdown = await showdown("anilibria", conf.Jackett.Anilibria); + conf.Jackett.Animelayer.showdown = await showdown("animelayer", conf.Jackett.Animelayer); + conf.Jackett.Anifilm.showdown = await showdown("anifilm", conf.Jackett.Anifilm); + } + } + catch { } + } + }); + } + + + /// + /// red + /// jackett + /// webapi + /// + public string typesearch = "webapi"; + + public string merge = "jackett"; + + public string webApiHost = "http://redapi.cfhttp.top"; + + public string filter { get; set; } + + public string filter_ignore { get; set; } + + + public RedConf Red = new RedConf(); + + public JacConf Jackett = new JacConf(); + } +} diff --git a/JacRed/Models/AniLibria/Names.cs b/JacRed/Models/AniLibria/Names.cs new file mode 100644 index 0000000..81b16f3 --- /dev/null +++ b/JacRed/Models/AniLibria/Names.cs @@ -0,0 +1,9 @@ +namespace JacRed.Models.AniLibria +{ + public class Names + { + public string ru { get; set; } + + public string en { get; set; } + } +} diff --git a/JacRed/Models/AniLibria/Quality.cs b/JacRed/Models/AniLibria/Quality.cs new file mode 100644 index 0000000..4801181 --- /dev/null +++ b/JacRed/Models/AniLibria/Quality.cs @@ -0,0 +1,11 @@ +namespace JacRed.Models.AniLibria +{ + public class Quality + { + public string @string { get; set; } + + public int resolution { get; set; } + + public string encoder { get; set; } + } +} diff --git a/JacRed/Models/AniLibria/RootObject.cs b/JacRed/Models/AniLibria/RootObject.cs new file mode 100644 index 0000000..ebe8a12 --- /dev/null +++ b/JacRed/Models/AniLibria/RootObject.cs @@ -0,0 +1,17 @@ +namespace JacRed.Models.AniLibria +{ + public class RootObject + { + public Names names { get; set; } + + public string code { get; set; } + + public Torrents torrents { get; set; } + + public Season season { get; set; } + + public long updated { get; set; } + + public long last_change { get; set; } + } +} diff --git a/JacRed/Models/AniLibria/Season.cs b/JacRed/Models/AniLibria/Season.cs new file mode 100644 index 0000000..c6fffb0 --- /dev/null +++ b/JacRed/Models/AniLibria/Season.cs @@ -0,0 +1,9 @@ +namespace JacRed.Models.AniLibria +{ + public class Season + { + public int year { get; set; } + + public int code { get; set; } + } +} diff --git a/JacRed/Models/AniLibria/Series.cs b/JacRed/Models/AniLibria/Series.cs new file mode 100644 index 0000000..c1bbaed --- /dev/null +++ b/JacRed/Models/AniLibria/Series.cs @@ -0,0 +1,7 @@ +namespace JacRed.Models.AniLibria +{ + public class Series + { + public string @string { get; set; } + } +} diff --git a/JacRed/Models/AniLibria/Torrent.cs b/JacRed/Models/AniLibria/Torrent.cs new file mode 100644 index 0000000..c0ec6a0 --- /dev/null +++ b/JacRed/Models/AniLibria/Torrent.cs @@ -0,0 +1,17 @@ +namespace JacRed.Models.AniLibria +{ + public class Torrent + { + public Series series { get; set; } + + public Quality quality { get; set; } + + public int leechers { get; set; } + + public int seeders { get; set; } + + public string url { get; set; } + + public long total_size { get; set; } + } +} diff --git a/JacRed/Models/AniLibria/Torrents.cs b/JacRed/Models/AniLibria/Torrents.cs new file mode 100644 index 0000000..b3f2854 --- /dev/null +++ b/JacRed/Models/AniLibria/Torrents.cs @@ -0,0 +1,7 @@ +namespace JacRed.Models.AniLibria +{ + public class Torrents + { + public List list { get; set; } + } +} diff --git a/JacRed/Models/AppConf/Evercache.cs b/JacRed/Models/AppConf/Evercache.cs new file mode 100644 index 0000000..5679a3d --- /dev/null +++ b/JacRed/Models/AppConf/Evercache.cs @@ -0,0 +1,13 @@ +namespace JacRed.Models.AppConf +{ + public class Evercache + { + public bool enable = false; + + public int validHour = 1; + + public int maxOpenWriteTask { get; set; } = 1000; + + public int dropCacheTake { get; set; } = 100; + } +} diff --git a/JacRed/Models/AppConf/JacConf.cs b/JacRed/Models/AppConf/JacConf.cs new file mode 100644 index 0000000..f5a29aa --- /dev/null +++ b/JacRed/Models/AppConf/JacConf.cs @@ -0,0 +1,40 @@ +namespace JacRed.Models.AppConf +{ + public class JacConf + { + public int cacheToMinutes = 5; + + public string search_lang = "query"; + + public int timeoutSeconds = 8; + + + public TrackerSettings Rutor = new TrackerSettings("https://rutor.info"/*, priority: "torrent"*/); + + public TrackerSettings Megapeer = new TrackerSettings("https://megapeer.vip", enable: false); + + public TrackerSettings TorrentBy = new TrackerSettings("https://torrent.by"/*, priority: "torrent"*/); + + public TrackerSettings Kinozal = new TrackerSettings("https://kinozal.tv"); + + public TrackerSettings NNMClub = new TrackerSettings("https://nnmclub.to"); + + public TrackerSettings Bitru = new TrackerSettings("https://bitru.org"); + + public TrackerSettings Toloka = new TrackerSettings("https://toloka.to"); + + public TrackerSettings Rutracker = new TrackerSettings("https://rutracker.org"/*, priority: "torrent"*/); + + public TrackerSettings BigFanGroup = new TrackerSettings("https://bigfangroup.org"); + + public TrackerSettings Selezen = new TrackerSettings("https://open.selezen.org"/*, priority: "torrent"*/); + + public TrackerSettings Lostfilm = new TrackerSettings("https://www.lostfilm.tv"); + + public TrackerSettings Anilibria = new TrackerSettings("https://www.anilibria.tv"); + + public TrackerSettings Animelayer = new TrackerSettings("http://animelayer.ru"); + + public TrackerSettings Anifilm = new TrackerSettings("https://anifilm.pro"); + } +} diff --git a/JacRed/Models/AppConf/LoginSettings.cs b/JacRed/Models/AppConf/LoginSettings.cs new file mode 100644 index 0000000..a52adde --- /dev/null +++ b/JacRed/Models/AppConf/LoginSettings.cs @@ -0,0 +1,9 @@ +namespace JacRed.Models.AppConf +{ + public class LoginSettings + { + public string u { get; set; } + + public string p { get; set; } + } +} diff --git a/JacRed/Models/AppConf/RedConf.cs b/JacRed/Models/AppConf/RedConf.cs new file mode 100644 index 0000000..190df52 --- /dev/null +++ b/JacRed/Models/AppConf/RedConf.cs @@ -0,0 +1,19 @@ +namespace JacRed.Models.AppConf +{ + public class RedConf + { + public string syncapi = "http://redapi.cfhttp.top"; + + public int syntime = 60; + + public string[] trackers = new string[] { "rutracker", "rutor", "kinozal", "nnmclub", "megapeer", "bitru", "toloka", "lostfilm", "baibako", "torrentby", "hdrezka", "selezen", "animelayer", "anilibria", "anifilm" }; + + public int maxreadfile = 300; + + public bool mergeduplicates = true; + + public bool mergenumduplicates = true; + + public Evercache evercache = new Evercache(); + } +} diff --git a/JacRed/Models/AppConf/TrackerSettings.cs b/JacRed/Models/AppConf/TrackerSettings.cs new file mode 100644 index 0000000..03d0529 --- /dev/null +++ b/JacRed/Models/AppConf/TrackerSettings.cs @@ -0,0 +1,43 @@ +using Shared.Models.Base; + +namespace JacRed.Models.AppConf +{ + public class TrackerSettings : Iproxy + { + public TrackerSettings(string host, bool enable = true, bool useproxy = false, LoginSettings login = null, string priority = null) + { + this.host = host; + this.enable = enable; + this.useproxy = useproxy; + + if (login != null) + this.login = login; + + this.priority = priority; + } + + + public string host { get; set; } + + public bool enable { get; set; } + + public bool showdown { get; set; } + + public bool monitor_showdown { get; set; } = true; + + public string priority { get; set; } + + public LoginSettings login { get; set; } = new LoginSettings(); + + public string cookie { get; set; } + + + public bool useproxy { get; set; } + + public bool useproxystream { get; set; } + + public string globalnameproxy { get; set; } + + public ProxySettings proxy { get; set; } + } +} diff --git a/JacRed/Models/Sync/Collection.cs b/JacRed/Models/Sync/Collection.cs new file mode 100644 index 0000000..925da8a --- /dev/null +++ b/JacRed/Models/Sync/Collection.cs @@ -0,0 +1,9 @@ +namespace JacRed.Models.Sync +{ + public class Collection + { + public string Key { get; set; } + + public Value Value { get; set; } + } +} diff --git a/JacRed/Models/Sync/RootObject.cs b/JacRed/Models/Sync/RootObject.cs new file mode 100644 index 0000000..e77b9d3 --- /dev/null +++ b/JacRed/Models/Sync/RootObject.cs @@ -0,0 +1,9 @@ +namespace JacRed.Models.Sync +{ + public class RootObject + { + public bool nextread { get; set; } + + public List collections { get; set; } + } +} diff --git a/JacRed/Models/Sync/Value.cs b/JacRed/Models/Sync/Value.cs new file mode 100644 index 0000000..b7adbf8 --- /dev/null +++ b/JacRed/Models/Sync/Value.cs @@ -0,0 +1,11 @@ +namespace JacRed.Models.Sync +{ + public class Value + { + public DateTime time { get; set; } + + public long fileTime { get; set; } + + public Dictionary torrents { get; set; } + } +} diff --git a/JacRed/Models/WriteTaskModel.cs b/JacRed/Models/WriteTaskModel.cs new file mode 100644 index 0000000..22e7aef --- /dev/null +++ b/JacRed/Models/WriteTaskModel.cs @@ -0,0 +1,15 @@ +namespace JacRed.Models +{ + public class WriteTaskModel + { + public FileDB db { get; set; } + + public int openconnection { get; set; } + + public int countread { get; set; } + + public DateTime lastread { get; set; } + + public DateTime create { get; set; } = DateTime.Now; + } +} diff --git a/Lampac.sln b/Lampac.sln new file mode 100644 index 0000000..b974fc5 --- /dev/null +++ b/Lampac.sln @@ -0,0 +1,98 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.2.11415.280 d18.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lampac", "Lampac\Lampac.csproj", "{6EC456A8-9B61-4F88-992B-972BE6E66C96}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "Shared\Shared.csproj", "{7A19617E-61AF-4741-B901-CB24D1565CA4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Catalog", "Catalog\Catalog.csproj", "{68BA3E1D-69D1-47D3-9E84-65189FF1AE90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Online", "Online\Online.csproj", "{DD693EB2-48C4-49D0-BFEE-B25B534CE6B4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SISI", "SISI\SISI.csproj", "{DF3D53D6-647F-42A3-9764-BE9B4ACA3235}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Merchant", "Merchant\Merchant.csproj", "{0C2B4AF5-F6CA-4465-8D53-BB8B245DE4A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DLNA", "DLNA\DLNA.csproj", "{A874EBE4-88D3-4C02-9FDA-6A6631380CE9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tracks", "Tracks\Tracks.csproj", "{E0036073-200D-4E60-8F6A-9C428E49442E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JacRed", "JacRed\JacRed.csproj", "{DB36397B-76E7-44EF-9F2E-21DDCBF6A836}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TorrServer", "TorrServer\TorrServer.csproj", "{A9FB8118-B1A4-436B-8C9C-1EACD4154F37}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseModule", "BaseModule\BaseModule.csproj", "{A9FB8118-B1A4-436B-8C9C-1EACD4154F39}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{1F4A0DC9-498D-49A9-91E4-F8007335BFD0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6EC456A8-9B61-4F88-992B-972BE6E66C96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EC456A8-9B61-4F88-992B-972BE6E66C96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EC456A8-9B61-4F88-992B-972BE6E66C96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EC456A8-9B61-4F88-992B-972BE6E66C96}.Release|Any CPU.Build.0 = Release|Any CPU + {7A19617E-61AF-4741-B901-CB24D1565CA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A19617E-61AF-4741-B901-CB24D1565CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A19617E-61AF-4741-B901-CB24D1565CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A19617E-61AF-4741-B901-CB24D1565CA4}.Release|Any CPU.Build.0 = Release|Any CPU + {68BA3E1D-69D1-47D3-9E84-65189FF1AE90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68BA3E1D-69D1-47D3-9E84-65189FF1AE90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68BA3E1D-69D1-47D3-9E84-65189FF1AE90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68BA3E1D-69D1-47D3-9E84-65189FF1AE90}.Release|Any CPU.Build.0 = Release|Any CPU + {DD693EB2-48C4-49D0-BFEE-B25B534CE6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD693EB2-48C4-49D0-BFEE-B25B534CE6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD693EB2-48C4-49D0-BFEE-B25B534CE6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD693EB2-48C4-49D0-BFEE-B25B534CE6B4}.Release|Any CPU.Build.0 = Release|Any CPU + {DF3D53D6-647F-42A3-9764-BE9B4ACA3235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF3D53D6-647F-42A3-9764-BE9B4ACA3235}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF3D53D6-647F-42A3-9764-BE9B4ACA3235}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF3D53D6-647F-42A3-9764-BE9B4ACA3235}.Release|Any CPU.Build.0 = Release|Any CPU + {0C2B4AF5-F6CA-4465-8D53-BB8B245DE4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C2B4AF5-F6CA-4465-8D53-BB8B245DE4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C2B4AF5-F6CA-4465-8D53-BB8B245DE4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C2B4AF5-F6CA-4465-8D53-BB8B245DE4A6}.Release|Any CPU.Build.0 = Release|Any CPU + {A874EBE4-88D3-4C02-9FDA-6A6631380CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A874EBE4-88D3-4C02-9FDA-6A6631380CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A874EBE4-88D3-4C02-9FDA-6A6631380CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A874EBE4-88D3-4C02-9FDA-6A6631380CE9}.Release|Any CPU.Build.0 = Release|Any CPU + {E0036073-200D-4E60-8F6A-9C428E49442E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0036073-200D-4E60-8F6A-9C428E49442E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0036073-200D-4E60-8F6A-9C428E49442E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0036073-200D-4E60-8F6A-9C428E49442E}.Release|Any CPU.Build.0 = Release|Any CPU + {DB36397B-76E7-44EF-9F2E-21DDCBF6A836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB36397B-76E7-44EF-9F2E-21DDCBF6A836}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB36397B-76E7-44EF-9F2E-21DDCBF6A836}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB36397B-76E7-44EF-9F2E-21DDCBF6A836}.Release|Any CPU.Build.0 = Release|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F37}.Release|Any CPU.Build.0 = Release|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9FB8118-B1A4-436B-8C9C-1EACD4154F39}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {68BA3E1D-69D1-47D3-9E84-65189FF1AE90} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {DD693EB2-48C4-49D0-BFEE-B25B534CE6B4} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {DF3D53D6-647F-42A3-9764-BE9B4ACA3235} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {0C2B4AF5-F6CA-4465-8D53-BB8B245DE4A6} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {A874EBE4-88D3-4C02-9FDA-6A6631380CE9} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {E0036073-200D-4E60-8F6A-9C428E49442E} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {DB36397B-76E7-44EF-9F2E-21DDCBF6A836} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {A9FB8118-B1A4-436B-8C9C-1EACD4154F37} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + {A9FB8118-B1A4-436B-8C9C-1EACD4154F39} = {1F4A0DC9-498D-49A9-91E4-F8007335BFD0} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F05D572A-75BC-4642-98F8-F65F5CCF6A0B} + EndGlobalSection +EndGlobal diff --git a/Lampac/Controllers/ApiController.cs b/Lampac/Controllers/ApiController.cs new file mode 100644 index 0000000..1f27364 --- /dev/null +++ b/Lampac/Controllers/ApiController.cs @@ -0,0 +1,162 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Shared; +using Shared.Engine; +using System; +using System.Linq; +using System.Text; +using System.Web; +using IO = System.IO; + +namespace Lampac.Controllers +{ + public class ApiController : BaseController + { + #region Version / Headers / geo / myip / reqinfo + [HttpGet] + [AllowAnonymous] + [Route("/version")] + public ActionResult Version() => Content($"{appversion}.{minorversion}"); + + [HttpGet] + [AllowAnonymous] + [Route("/ping")] + public ActionResult PingPong() => Content("pong"); + + [HttpGet] + [AllowAnonymous] + [Route("/headers")] + public ActionResult Headers(string type) + { + if (type == "text") + { + return Content(string.Join( + Environment.NewLine, + HttpContext.Request.Headers.Select(h => $"{h.Key}: {h.Value}") + )); + } + + return Json(HttpContext.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString())); + } + + [HttpGet] + [AllowAnonymous] + [Route("/geo")] + public ActionResult Geo(string select, string ip) + { + if (select == "ip") + return Content(ip ?? requestInfo.IP); + + string country = requestInfo.Country; + if (ip != null) + country = GeoIP2.Country(ip); + + if (select == "country") + return Content(country); + + return Json(new + { + ip = ip ?? requestInfo.IP, + country + }); + } + + [HttpGet] + [AllowAnonymous] + [Route("/myip")] + public ActionResult MyIP() => Content(requestInfo.IP); + + [HttpGet] + [Route("/reqinfo")] + public ActionResult Reqinfo() => ContentTo(JsonConvert.SerializeObject(requestInfo, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore + })); + #endregion + + + #region invc-ws.js + [HttpGet] + [AllowAnonymous] + [Route("invc-ws.js")] + [Route("invc-ws/js/{token}")] + public ActionResult InvcSyncJS(string token) + { + StringBuilder sb; + + if (AppInit.conf.sync_user.version == 1) + { + sb = new StringBuilder(FileCache.ReadAllText("plugins/invc-ws.js")); + } + else + { + sb = new StringBuilder(FileCache.ReadAllText("plugins/sync_v2/invc-ws.js")); + } + + sb.Replace("{invc-rch}", FileCache.ReadAllText("plugins/invc-rch.js")) + .Replace("{invc-rch_nws}", FileCache.ReadAllText("plugins/invc-rch_nws.js")) + .Replace("{localhost}", host) + .Replace("{token}", HttpUtility.UrlEncode(token)); + + return Content(sb.ToString(), "application/javascript; charset=utf-8"); + } + #endregion + + #region invc-rch.js + [HttpGet] + [AllowAnonymous] + [Route("invc-rch.js")] + public ActionResult InvcRchJS() + { + string source = FileCache.ReadAllText("plugins/invc-rch.js").Replace("{localhost}", host); + + source = $"(function(){{'use strict'; {source} }})();"; + + return Content(source, "application/javascript; charset=utf-8"); + } + #endregion + + + #region nws-client-es5.js + [HttpGet] + [AllowAnonymous] + [Route("nws-client-es5.js")] + [Route("js/nws-client-es5.js")] + public ActionResult NwsClient() + { + string memKey = "ApiController:nws-client-es5.js"; + if (!memoryCache.TryGetValue(memKey, out string source)) + { + source = IO.File.ReadAllText("plugins/nws-client-es5.js"); + memoryCache.Set(memKey, source); + } + + if (source.Contains("{localhost}")) + source = source.Replace("{localhost}", host); + + return Content(source, "application/javascript; charset=utf-8"); + } + #endregion + + #region signalr-6.0.25_es5.js + [HttpGet] + [AllowAnonymous] + [Route("signalr-6.0.25_es5.js")] + public ActionResult SignalrJs() + { + string memKey = "ApiController:signalr-6.0.25_es5.js"; + if (!memoryCache.TryGetValue(memKey, out string source)) + { + source = IO.File.ReadAllText("plugins/signalr-6.0.25_es5.js"); + memoryCache.Set(memKey, source); + } + + return Content(source, "application/javascript; charset=utf-8"); + } + #endregion + } +} \ No newline at end of file diff --git a/Lampac/Controllers/OpenStatController.cs b/Lampac/Controllers/OpenStatController.cs new file mode 100644 index 0000000..10b1aac --- /dev/null +++ b/Lampac/Controllers/OpenStatController.cs @@ -0,0 +1,196 @@ +using Lampac.Engine; +using Lampac.Engine.Middlewares; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Shared; +using Shared.Engine; +using Shared.Engine.Pools; +using Shared.Models.AppConf; +using Shared.PlaywrightCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; + +namespace Lampac.Controllers +{ + public class OpenStatController : BaseController + { + public OpenStatConf openstat => AppInit.conf.openstat; + + public bool IsDeny(out string ermsg) + { + ermsg = "Включите openstat в init.conf\n\n\"openstat\": {\n \"enable\": true\n}"; + + if (!openstat.enable || (!string.IsNullOrEmpty(openstat.token) && openstat.token != HttpContext.Request.Query["token"].ToString())) + return true; + + return false; + } + + #region browser/context + [HttpGet] + [AllowAnonymous] + [Route("/stats/browser/context")] + public ActionResult BrowserContext() + { + if (IsDeny(out string ermsg)) + return Content(ermsg, "text/plain; charset=utf-8"); + + return Json(new + { + Chromium = new + { + open = Chromium.ContextsCount, + req_keepopen = Chromium.stats_keepopen, + req_newcontext = Chromium.stats_newcontext, + ping = new + { + Chromium.stats_ping.status, + Chromium.stats_ping.time, + Chromium.stats_ping.ex + } + }, + Firefox = new + { + open = Firefox.ContextsCount, + req_keepopen = Firefox.stats_keepopen, + req_newcontext = Firefox.stats_newcontext + } + }); + } + #endregion + + #region request + [HttpGet] + [AllowAnonymous] + [Route("/stats/request")] + public ActionResult Requests() + { + if (IsDeny(out string ermsg)) + return Content(ermsg, "text/plain; charset=utf-8"); + + var now = DateTime.UtcNow; + + long req_min = 0; + if (memoryCache.TryGetValue($"stats:request:{now.Hour}:{now.AddMinutes(-1).Minute}", out CounterRequestInfo _counter)) + req_min = _counter.Value; + + long req_hour = req_min; + for (int i = 1; i < 60; i++) + { + var cutoff = now.AddMinutes(-i); + if (memoryCache.TryGetValue($"stats:request:{cutoff.Hour}:{cutoff.Minute}", out CounterRequestInfo _r)) + req_hour += _r.Value; + } + + var responseStats = RequestStatisticsTracker.GetResponseTimeStatsLastMinute(); + + var httpResponseMs = new Dictionary + { + ["avg"] = Math.Round(responseStats.Average, 2) + }; + + foreach (var percentile in responseStats.PercentileAverages.OrderBy(x => x.Key)) + httpResponseMs.Add(percentile.Key.ToString(), Math.Round(percentile.Value, 2)); + + return Json(new + { + req_min, + req_hour, + tcpConnections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections().Length, + nws_online = NativeWebSocket.CountConnection, + soks_online = soks.connections, + http_active = RequestStatisticsTracker.ActiveHttpRequests, + http_response_ms = httpResponseMs + }); + } + #endregion + + #region rch + [HttpGet] + [AllowAnonymous] + [Route("/stats/rch")] + public ActionResult Rhc() + { + if (IsDeny(out string ermsg)) + return Content(ermsg, "text/plain; charset=utf-8"); + + var now = DateTime.UtcNow; + + int receive = 0, send = 0; + + if (memoryCache.TryGetValue("stats:nws", out CounterNws _c)) + { + receive = _c.receive; + send = _c.send; + } + + return Json(new + { + clients = RchClient.clients.Count, + counter = new + { + receive, + send + }, + rchIds = RchClient.rchIds.Count + }); + } + #endregion + + #region TempDb + [HttpGet] + [AllowAnonymous] + [Route("/stats/tempdb")] + public ActionResult TempDb() + { + if (IsDeny(out string ermsg)) + return Content(ermsg, "text/plain; charset=utf-8"); + + return Json(new + { + HybridCache = HybridCache.Stat_ContTempDb, + HybridFileCache = HybridFileCache.Stat_ContTempDb, + ProxyLink = ProxyLink.Stat_ContLinks, + ProxyAPI = ProxyAPI.Stat_ContCacheFiles, + ProxyTmdb = ProxyTmdb.Stat_ContCacheFiles, + ProxyImg = ProxyImg.Stat_ContCacheFiles, + ProxyCub = ProxyCub.Stat_ContCacheFiles, + SemaphorManager = SemaphorManager.Stat_ContSemaphoreLocks, + rch = new + { + clients = RchClient.clients.Count, + Ids = RchClient.rchIds.Count + }, + pool = new + { + msm = new + { + PoolInvk.msm.SmallPoolInUseSize, + PoolInvk.msm.LargePoolInUseSize, + PoolInvk.msm.SmallBlocksFree, + PoolInvk.msm.SmallPoolFreeSize, + PoolInvk.msm.LargeBuffersFree, + PoolInvk.msm.LargePoolFreeSize + }, + StringBuilder = new + { + Rent = StringBuilderPool.RentNew, + Free = StringBuilderPool.FreeCont, + StringBuilderPool.GC + }, + MemoryStream = MemoryStreamPool.Count == 0 ? null : new + { + MemoryStreamPool.Count, + MemoryStreamPool.GC + } + }, + memoryCache = memoryCache.GetCurrentStatistics() + }); + } + #endregion + } +} \ No newline at end of file diff --git a/Lampac/Engine/CRON/CacheCron.cs b/Lampac/Engine/CRON/CacheCron.cs new file mode 100644 index 0000000..555b2da --- /dev/null +++ b/Lampac/Engine/CRON/CacheCron.cs @@ -0,0 +1,116 @@ +using Shared; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Lampac.Engine.CRON +{ + public static class CacheCron + { + public static void Run() + { + _cronTimer = new Timer(cron, null, TimeSpan.FromMinutes(2), TimeSpan.FromMinutes(5)); + } + + static Timer _cronTimer; + + + static int _updatingDb = 0; + + static void cron(object state) + { + if (Interlocked.Exchange(ref _updatingDb, 1) == 1) + return; + + try + { + var files = new Dictionary(); + long freeDiskSpace = getFreeDiskSpace(); + + foreach (var conf in new List<(string path, int minute)> { + ("tmdb", AppInit.conf.tmdb.cache_img), + ("cub", AppInit.conf.cub.cache_img), + ("img", AppInit.conf.serverproxy.image.cache_time), + ("torrent", AppInit.conf.fileCacheInactive.torrent), + ("html", AppInit.conf.fileCacheInactive.html), + ("hls", AppInit.conf.fileCacheInactive.hls), + ("storage/temp", 10) + }) + { + try + { + string path = Path.Combine("cache", conf.path); + if (conf.minute == -1 || !Directory.Exists(path)) + continue; + + var ex = DateTime.UtcNow.AddMinutes(-conf.minute); + + foreach (string infile in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + { + try + { + if (conf.minute == 0) + File.Delete(infile); + else + { + var lastWriteTime = File.GetLastWriteTimeUtc(infile); + if (ex > lastWriteTime) + File.Delete(infile); + else if (freeDiskSpace != -1 && AppInit.conf.fileCacheInactive.freeDiskSpace > freeDiskSpace) + files.TryAdd(infile, new FileInfo(infile)); + } + } + catch { } + } + } + catch { } + } + + if (files.Count > 0) + { + long removeGb = 0; + + foreach (var item in files.OrderBy(i => i.Value.LastWriteTime)) + { + try + { + if (File.Exists(item.Key)) + { + File.Delete(item.Key); + removeGb += item.Value.Length; + + // 2Gb + if (removeGb > 2147483648) + break; + } + } + catch { } + } + } + } + catch { } + finally + { + Volatile.Write(ref _updatingDb, 0); + } + } + + + static long getFreeDiskSpace() + { + try + { + var directory = new DirectoryInfo("cache"); + var drive = DriveInfo.GetDrives() + .FirstOrDefault(d => d.IsReady && directory.FullName.StartsWith(d.RootDirectory.FullName, StringComparison.OrdinalIgnoreCase)); + return drive?.AvailableFreeSpace ?? -1; + } + catch + { + return -1; + } + } + } +} diff --git a/Lampac/Engine/CRON/KurwaCron.cs b/Lampac/Engine/CRON/KurwaCron.cs new file mode 100644 index 0000000..b6c1a6b --- /dev/null +++ b/Lampac/Engine/CRON/KurwaCron.cs @@ -0,0 +1,59 @@ +using Shared; +using Shared.Engine; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.CRON +{ + public static class KurwaCron + { + public static void Run() + { + _cronTimer = new Timer(cron, null, TimeSpan.FromMinutes(20), TimeSpan.FromHours(5)); + } + + static Timer _cronTimer; + + static bool _cronWork = false; + + async static void cron(object state) + { + if (_cronWork) + return; + + _cronWork = true; + + try + { + await DownloadBigJson("externalids"); + await DownloadBigJson("cdnmovies"); + await DownloadBigJson("lumex"); + await DownloadBigJson("veoveo"); + await DownloadBigJson("kodik"); + } + finally + { + _cronWork = false; + } + } + + async static Task DownloadBigJson(string path) + { + try + { + using (var ms = PoolInvk.msm.GetStream()) + { + bool success = await Http.DownloadToStream(ms, $"http://194.246.82.144/{path}.json"); + if (success) + { + using (var fileStream = new FileStream($"data/{path}.json", FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + await ms.CopyToAsync(fileStream, PoolInvk.bufferSize); + } + } + } + catch { } + } + } +} diff --git a/Lampac/Engine/CRON/LampaCron.cs b/Lampac/Engine/CRON/LampaCron.cs new file mode 100644 index 0000000..639b5a4 --- /dev/null +++ b/Lampac/Engine/CRON/LampaCron.cs @@ -0,0 +1,122 @@ +using Shared; +using Shared.Engine; +using System; +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.CRON +{ + public static class LampaCron + { + static string currentapp; + + public static void Run() + { + var init = AppInit.conf.LampaWeb; + _cronTimer = new Timer(cron, null, TimeSpan.FromSeconds(20), TimeSpan.FromMinutes(Math.Max(init.intervalupdate, 5))); + } + + static Timer _cronTimer; + + static int _updatingDb = 0; + + async static void cron(object state) + { + if (Interlocked.Exchange(ref _updatingDb, 1) == 1) + return; + + try + { + var init = AppInit.conf.LampaWeb; + bool istree = !string.IsNullOrEmpty(init.tree); + + async ValueTask update() + { + if (!init.autoupdate) + return false; + + if (!File.Exists("wwwroot/lampa-main/app.min.js")) + return true; + + if (istree && File.Exists("wwwroot/lampa-main/tree") && init.tree == File.ReadAllText("wwwroot/lampa-main/tree")) + return false; + + bool changeversion = false; + + await Http.GetSpan(gitapp => + { + if (!gitapp.Contains("author: 'Yumata'", StringComparison.Ordinal)) + return; + + if (currentapp == null) + currentapp = CrypTo.md5File("wwwroot/lampa-main/app.min.js"); + + if (!string.IsNullOrEmpty(currentapp) && CrypTo.md5(gitapp) != currentapp) + changeversion = true; + + }, $"https://raw.githubusercontent.com/{init.git}/{(istree ? init.tree : "main")}/app.min.js", weblog: false); + + if (istree) + File.WriteAllText("wwwroot/lampa-main/tree", init.tree); + + return changeversion; + } + + if (await update()) + { + string uri = istree ? + $"https://github.com/{init.git}/archive/{init.tree}.zip" : + $"https://github.com/{init.git}/archive/refs/heads/main.zip"; + + byte[] array = await Http.Download(uri); + if (array != null) + { + currentapp = null; + + await File.WriteAllBytesAsync("wwwroot/lampa.zip", array); + ZipFile.ExtractToDirectory("wwwroot/lampa.zip", "wwwroot/", overwriteFiles: true); + + if (istree) + { + foreach (string infilePath in Directory.GetFiles($"wwwroot/lampa-{init.tree}", "*", SearchOption.AllDirectories)) + { + string outfile = infilePath.Replace($"lampa-{init.tree}", "lampa-main"); + Directory.CreateDirectory(Path.GetDirectoryName(outfile)); + File.Copy(infilePath, outfile, true); + } + + File.WriteAllText("wwwroot/lampa-main/tree", init.tree); + } + + string html = File.ReadAllText("wwwroot/lampa-main/index.html"); + html = html.Replace("", ""); + + File.WriteAllText("wwwroot/lampa-main/index.html", html); + File.CreateText("wwwroot/lampa-main/personal.lampa"); + + if (!File.Exists("wwwroot/lampa-main/plugins_black_list.json")) + File.WriteAllText("wwwroot/lampa-main/plugins_black_list.json", "[]"); + + if (!File.Exists("wwwroot/lampa-main/plugins/modification.js")) + { + Directory.CreateDirectory("wwwroot/lampa-main/plugins"); + File.WriteAllText("wwwroot/lampa-main/plugins/modification.js", string.Empty); + } + + File.Delete("wwwroot/lampa.zip"); + + if (istree) + Directory.Delete($"wwwroot/lampa-{init.tree}", true); + } + } + } + catch { } + finally + { + Volatile.Write(ref _updatingDb, 0); + } + } + } +} diff --git a/Lampac/Engine/CRON/PluginsCron.cs b/Lampac/Engine/CRON/PluginsCron.cs new file mode 100644 index 0000000..b4e0111 --- /dev/null +++ b/Lampac/Engine/CRON/PluginsCron.cs @@ -0,0 +1,81 @@ +using Shared; +using Shared.Engine; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.CRON +{ + public static class PluginsCron + { + public static void Run() + { + _cronTimer = new Timer(cron, null, TimeSpan.FromMinutes(2), TimeSpan.FromHours(1)); + } + + static Timer _cronTimer; + + static bool _cronWork = false; + + async static void cron(object state) + { + if (_cronWork) + return; + + _cronWork = true; + + try + { + if (!AppInit.conf.pirate_store) + return; + + await update("https://immisterio.github.io/bwa/fx.js"); + await update("https://adultjs.onrender.com", path: "adult.js"); + await update("https://nb557.github.io/plugins/online_mod.js"); + await update("http://github.freebie.tom.ru/want.js"); + await update("https://nb557.github.io/plugins/reset_subs.js"); + await update("http://193.233.134.21/plugins/mult.js"); + await update("https://nemiroff.github.io/lampa/select_weapon.js"); + await update("https://nb557.github.io/plugins/not_mobile.js"); + await update("http://cub.red/plugin/etor", path: "etor.js"); + await update("http://193.233.134.21/plugins/checker.js"); + await update("https://plugin.rootu.top/ts-preload.js"); + await update("https://lampame.github.io/main/pubtorr/pubtorr.js"); + await update("https://lampame.github.io/main/nc/nc.js"); + await update("https://nb557.github.io/plugins/rating.js"); + await update("https://github.freebie.tom.ru/torrents.js"); + await update("https://nnmdd.github.io/lampa_hotkeys/hotkeys.js"); + await update("https://bazzzilius.github.io/scripts/gold_theme.js"); + await update("https://bdvburik.github.io/rezkacomment.js"); + await update("https://lampame.github.io/main/Shikimori/Shikimori.js"); + } + catch { } + finally + { + _cronWork = false; + } + } + + + async static Task update(string url, string checkcode = "Lampa.", string path = null) + { + try + { + await Http.GetSpan(js => + { + if (js.Contains(checkcode, StringComparison.Ordinal)) + { + if (path == null) + path = Path.GetFileName(url); + + File.WriteAllText($"wwwroot/plugins/{path}", js, Encoding.UTF8); + } + + }, url, Encoding.UTF8, weblog: false); + } + catch { } + } + } +} diff --git a/Lampac/Engine/CRON/SyncCron.cs b/Lampac/Engine/CRON/SyncCron.cs new file mode 100644 index 0000000..0b868a1 --- /dev/null +++ b/Lampac/Engine/CRON/SyncCron.cs @@ -0,0 +1,53 @@ +using Shared; +using Shared.Engine; +using Shared.Models; +using System; +using System.Threading; + +namespace Lampac.Engine.CRON +{ + public static class SyncCron + { + public static void Run() + { + _cronTimer = new Timer(cron, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); + } + + static Timer _cronTimer; + + static int _updatingDb = 0; + + async static void cron(object state) + { + if (Interlocked.Exchange(ref _updatingDb, 1) == 1) + return; + + try + { + var sync = AppInit.conf?.sync; + + if (sync == null || !sync.enable || sync.type != "slave" || string.IsNullOrEmpty(sync.api_host) || string.IsNullOrEmpty(sync.api_passwd)) + return; + + var init = await Http.Get(sync.api_host + "/api/sync", timeoutSeconds: 5, headers: HeadersModel.Init("localrequest", sync.api_passwd), weblog: false); + if (init != null) + { + if (sync.sync_full) + { + init.sync = sync; + AppInit.conf = init; + } + else + { + AppInit.conf.accsdb.users = init.accsdb.users; + } + } + } + catch { } + finally + { + Volatile.Write(ref _updatingDb, 0); + } + } + } +} diff --git a/Lampac/Engine/CRON/TrackersCron.cs b/Lampac/Engine/CRON/TrackersCron.cs new file mode 100644 index 0000000..a0acab8 --- /dev/null +++ b/Lampac/Engine/CRON/TrackersCron.cs @@ -0,0 +1,127 @@ +using Shared; +using Shared.Engine; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.CRON +{ + public static class TrackersCron + { + public static void Run() + { + _cronTimer = new Timer(cron, null, TimeSpan.FromMinutes(2), TimeSpan.FromMinutes(AppInit.conf.dlna.intervalUpdateTrackers)); + } + + static Timer _cronTimer; + + static int _updatingDb = 0; + + async static void cron(object state) + { + if (Interlocked.Exchange(ref _updatingDb, 1) == 1) + return; + + try + { + if (AppInit.modules == null || AppInit.modules.FirstOrDefault(i => i.dll == "DLNA.dll" && i.enable) == null) + return; + + if (AppInit.conf.dlna.enable && AppInit.conf.dlna.autoupdatetrackers) + { + var trackers = new HashSet(); + var trackers_bad = new HashSet(); + var temp = new HashSet(); + + foreach (string uri in new string[] + { + "http://redapi.cfhttp.top/trackers.txt", + "https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt", + "https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt", + "https://newtrackon.com/api/all" + }) + { + string plain = await Http.Get(uri, weblog: false); + if (plain == null) + continue; + + foreach (string line in plain.Replace("\r", "").Replace("\t", "").Split("\n")) + if (!string.IsNullOrEmpty(line)) + temp.Add(line.Trim()); + } + + foreach (string url in temp) + { + if (await ckeck(url)) + trackers.Add(url); + else + trackers_bad.Add(url); + } + + File.WriteAllLines("cache/trackers_bad.txt", trackers_bad); + File.WriteAllLines("cache/trackers.txt", trackers.OrderByDescending(i => Regex.IsMatch(i, "[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")).ThenByDescending(i => i.StartsWith("http"))); + } + } + catch { } + finally + { + Volatile.Write(ref _updatingDb, 0); + } + } + + + async static Task ckeck(string tracker) + { + if (string.IsNullOrWhiteSpace(tracker) || tracker.Contains("[")) + return false; + + if (tracker.StartsWith("http")) + { + try + { + using (var handler = new System.Net.Http.HttpClientHandler()) + { + handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + + using (var client = new System.Net.Http.HttpClient(handler)) + { + client.Timeout = TimeSpan.FromSeconds(7); + await client.GetAsync(tracker, System.Net.Http.HttpCompletionOption.ResponseHeadersRead); + return true; + } + } + } + catch { } + } + else if (tracker.StartsWith("udp:")) + { + try + { + tracker = tracker.Replace("udp://", ""); + + string host = tracker.Split(':')[0].Split('/')[0]; + int port = tracker.Contains(":") ? int.Parse(tracker.Split(':')[1].Split('/')[0]) : 6969; + + using (UdpClient client = new UdpClient(host, port)) + { + CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(7000); + + string uri = Regex.Match(tracker, "^[^/]/(.*)").Groups[1].Value; + await client.SendAsync(Encoding.UTF8.GetBytes($"GET /{uri} HTTP/1.1\r\nHost: {host}\r\n\r\n"), cts.Token); + return true; + } + } + catch { } + } + + return false; + } + } +} diff --git a/Lampac/Engine/DynamicActionDescriptorChangeProvider.cs b/Lampac/Engine/DynamicActionDescriptorChangeProvider.cs new file mode 100644 index 0000000..a21953b --- /dev/null +++ b/Lampac/Engine/DynamicActionDescriptorChangeProvider.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Primitives; +using System.Threading; + +namespace Lampac.Engine +{ + public class DynamicActionDescriptorChangeProvider : IActionDescriptorChangeProvider + { + public static DynamicActionDescriptorChangeProvider Instance { get; } = new DynamicActionDescriptorChangeProvider(); + + private CancellationTokenSource tokenSource = new CancellationTokenSource(); + public CancellationTokenSource TokenSource => tokenSource; + + public IChangeToken GetChangeToken() => new CancellationChangeToken(tokenSource.Token); + + public void NotifyChanges() + { + var previous = Interlocked.Exchange(ref tokenSource, new CancellationTokenSource()); + previous.Cancel(); + } + } +} diff --git a/Lampac/Engine/Middlewares/Accsdb.cs b/Lampac/Engine/Middlewares/Accsdb.cs new file mode 100644 index 0000000..0d6c4b0 --- /dev/null +++ b/Lampac/Engine/Middlewares/Accsdb.cs @@ -0,0 +1,335 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Shared; +using Shared.Engine; +using Shared.Models; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class Accsdb + { + static readonly Regex rexJac = new Regex("^/(api/v2.0/indexers|api/v1.0/|toloka|rutracker|rutor|torrentby|nnmclub|kinozal|bitru|selezen|megapeer|animelayer|anilibria|anifilm|toloka|lostfilm|bigfangroup|mazepa)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex rexStaticAssets = new Regex("\\.(js|css|ico|png|svg|jpe?g|woff|webmanifest)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex rexProxyPath = new Regex("/(proxy|proxyimg([^/]+)?)/", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex rexTmdbPath = new Regex("^/tmdb/[^/]+/", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex rexLockBypass = new Regex("^/(testaccsdb|proxy/|proxyimg|lifeevents|externalids|sisi/(bookmarks|historys)|(ts|transcoding|dlna|storage|bookmark|tmdb|cub)/|timecode)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + static Accsdb() + { + Directory.CreateDirectory("cache/logs/accsdb"); + } + + private readonly RequestDelegate _next; + static bool manifestInitial = false; + IMemoryCache memoryCache; + + public Accsdb(RequestDelegate next, IMemoryCache mem) + { + _next = next; + memoryCache = mem; + } + + public Task Invoke(HttpContext httpContext) + { + var requestInfo = httpContext.Features.Get(); + + #region manifest/install + if (!manifestInitial) + { + if (!File.Exists("module/manifest.json")) + { + if (httpContext.Request.Path.Value.StartsWith("/admin/manifest/install", StringComparison.OrdinalIgnoreCase)) + return _next(httpContext); + + httpContext.Response.Redirect("/admin/manifest/install"); + return Task.CompletedTask; + } + else { manifestInitial = true; } + } + #endregion + + #region admin + if (httpContext.Request.Path.Value.StartsWith("/admin", StringComparison.OrdinalIgnoreCase)) + { + if (httpContext.Request.Cookies.TryGetValue("passwd", out string passwd)) + { + string ipKey = $"Accsdb:auth:IP:{requestInfo.IP}"; + if (!memoryCache.TryGetValue(ipKey, out ConcurrentDictionary passwds)) + { + passwds = new ConcurrentDictionary(); + memoryCache.Set(ipKey, passwds, DateTime.Today.AddDays(1)); + } + + passwds.TryAdd(passwd, 0); + + if (passwds.Count > 10) + return httpContext.Response.WriteAsync("Too many attempts, try again tomorrow.", httpContext.RequestAborted); + + if (passwd == AppInit.rootPasswd) + return _next(httpContext); + } + + if (httpContext.Request.Path.Value.StartsWith("/admin/auth", StringComparison.OrdinalIgnoreCase)) + return _next(httpContext); + + httpContext.Response.Redirect("/admin/auth"); + return Task.CompletedTask; + } + #endregion + + if (requestInfo.IsLocalRequest || requestInfo.IsAnonymousRequest) + return _next(httpContext); + + #region jacred + if (rexJac.IsMatch(httpContext.Request.Path.Value)) + { + if (!string.IsNullOrEmpty(AppInit.conf.apikey)) + { + if (AppInit.conf.apikey != httpContext.Request.Query["apikey"]) + return Task.CompletedTask; + } + + return _next(httpContext); + } + #endregion + + if (AppInit.conf.accsdb.enable || (!requestInfo.IsLocalIp && !AppInit.conf.WAF.allowExternalIpAccess)) + { + var accsdb = AppInit.conf.accsdb; + + if (httpContext.Request.Path.Value.StartsWith("/testaccsdb", StringComparison.OrdinalIgnoreCase) && accsdb.shared_passwd != null && requestInfo.user_uid == accsdb.shared_passwd) + { + requestInfo.IsLocalRequest = true; + return _next(httpContext); + } + + if (!string.IsNullOrEmpty(accsdb.premium_pattern) && !Regex.IsMatch(httpContext.Request.Path.Value, accsdb.premium_pattern, RegexOptions.IgnoreCase)) + return _next(httpContext); + + if (!string.IsNullOrEmpty(accsdb.whitepattern) && Regex.IsMatch(httpContext.Request.Path.Value, accsdb.whitepattern, RegexOptions.IgnoreCase)) + { + requestInfo.IsAnonymousRequest = true; + return _next(httpContext); + } + + bool limitip = false; + + var user = requestInfo.user; + + if (requestInfo.user_uid != null && accsdb.white_uids != null && accsdb.white_uids.Contains(requestInfo.user_uid)) + return _next(httpContext); + + string uri = httpContext.Request.Path.Value + httpContext.Request.QueryString.Value; + + if (IsLockHostOrUser(memoryCache, requestInfo.user_uid, requestInfo.IP, uri, out limitip) + || user == null + || user.ban + || DateTime.UtcNow > user.expires) + { + if (httpContext.Request.Path.Value.StartsWith("/proxy/", StringComparison.OrdinalIgnoreCase) || + httpContext.Request.Path.Value.StartsWith("/proxyimg", StringComparison.OrdinalIgnoreCase)) + { + string hash = rexProxyPath.Replace(httpContext.Request.Path.Value, ""); + if (AppInit.conf.serverproxy.encrypt || ProxyLink.Decrypt(hash, requestInfo.IP)?.uri != null) + return _next(httpContext); + } + + if (uri.StartsWith("/tmdb/api.themoviedb.org/", StringComparison.OrdinalIgnoreCase) || + uri.StartsWith("/tmdb/api/", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.Redirect("https://api.themoviedb.org/" + rexTmdbPath.Replace(httpContext.Request.Path.Value, "")); + return Task.CompletedTask; + } + + if (rexStaticAssets.IsMatch(httpContext.Request.Path.Value)) + { + if (uri.StartsWith("/tmdb/image.tmdb.org/", StringComparison.OrdinalIgnoreCase) || + uri.StartsWith("/tmdb/img/", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.Redirect("https://image.tmdb.org/" + rexTmdbPath.Replace(httpContext.Request.Path.Value, "")); + return Task.CompletedTask; + } + + httpContext.Response.StatusCode = 404; + httpContext.Response.ContentType = "application/octet-stream"; + return Task.CompletedTask; + } + + #region msg + string msg = limitip ? $"Превышено допустимое количество ip/запросов на аккаунт." + : string.IsNullOrEmpty(requestInfo.user_uid) ? accsdb.authMesage + : accsdb.denyMesage.Replace("{account_email}", requestInfo.user_uid).Replace("{user_uid}", requestInfo.user_uid).Replace("{host}", httpContext.Request.Host.Value); + + if (user != null) + { + if (user.ban) + msg = user.ban_msg ?? "Вы заблокированы"; + + else if (DateTime.UtcNow > user.expires) + { + msg = accsdb.expiresMesage + .Replace("{account_email}", requestInfo.user_uid) + .Replace("{user_uid}", requestInfo.user_uid) + .Replace("{expires}", user.expires.ToString("dd.MM.yyyy")); + } + } + #endregion + + #region denymsg + string denymsg = limitip ? $"Превышено допустимое количество ip/запросов на аккаунт." : null; + + if (user != null) + { + if (user.ban) + denymsg = user.ban_msg ?? "Вы заблокированы"; + + else if (DateTime.UtcNow > user.expires) + { + denymsg = accsdb.expiresMesage + .Replace("{account_email}", requestInfo.user_uid) + .Replace("{user_uid}", requestInfo.user_uid) + .Replace("{expires}", user.expires.ToString("dd.MM.yyyy")); + } + } + #endregion + + return httpContext.Response.WriteAsJsonAsync(new { accsdb = true, msg, denymsg, user }, httpContext.RequestAborted); + } + } + + return _next(httpContext); + } + + + #region IsLock + static bool IsLockHostOrUser(IMemoryCache memoryCache, string account_email, string userip, string uri, out bool islock) + { + if (string.IsNullOrEmpty(account_email)) + { + islock = false; + return islock; + } + + if (rexLockBypass.IsMatch(uri)) + { + islock = false; + return islock; + } + + if (IsLockIpHour(memoryCache, account_email, userip, out islock, out ConcurrentDictionary ips) | + IsLockReqHour(memoryCache, account_email, uri, out islock, out ConcurrentDictionary urls)) + { + setLogs("lock_hour", account_email); + countlock_day(memoryCache, true, account_email); + + File.WriteAllLines($"cache/logs/accsdb/{CrypTo.md5(account_email)}.ips.log", ips.Keys); + File.WriteAllLines($"cache/logs/accsdb/{CrypTo.md5(account_email)}.urls.log", urls.Keys); + + return islock; + } + + if (countlock_day(memoryCache, false, account_email) > AppInit.conf.accsdb.maxlock_day) + { + if (AppInit.conf.accsdb.blocked_hour != -1) + memoryCache.Set($"Accsdb:blocked_hour:{account_email}", 0, DateTime.Now.AddHours(AppInit.conf.accsdb.blocked_hour)); + + setLogs("lock_day", account_email); + islock = true; + return islock; + } + + if (memoryCache.TryGetValue($"Accsdb:blocked_hour:{account_email}", out _)) + { + setLogs("blocked", account_email); + islock = true; + return islock; + } + + islock = false; + return islock; + } + + + static bool IsLockIpHour(IMemoryCache memoryCache, string account_email, string userip, out bool islock, out ConcurrentDictionary ips) + { + ips = memoryCache.GetOrCreate($"Accsdb:IsLockIpHour:{account_email}:{DateTime.Now.Hour}", entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); + return new ConcurrentDictionary(); + }); + + ips.TryAdd(userip, 0); + + if (ips.Count > AppInit.conf.accsdb.maxip_hour) + { + islock = true; + return islock; + } + + islock = false; + return islock; + } + + static bool IsLockReqHour(IMemoryCache memoryCache, string account_email, string uri, out bool islock, out ConcurrentDictionary urls) + { + urls = memoryCache.GetOrCreate($"Accsdb:IsLockReqHour:{account_email}:{DateTime.Now.Hour}", entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); + return new ConcurrentDictionary(); + }); + + urls.TryAdd(uri, 0); + + if (urls.Count > AppInit.conf.accsdb.maxrequest_hour) + { + islock = true; + return islock; + } + + islock = false; + return islock; + } + #endregion + + + #region setLogs + static string logsLock = string.Empty; + + static void setLogs(string name, string account_email) + { + string logFile = $"cache/logs/accsdb/{DateTime.Now:dd-MM-yyyy}.lock.txt"; + if (logsLock != string.Empty && !File.Exists(logFile)) + logsLock = string.Empty; + + string line = $"{name} / {account_email} / {CrypTo.md5(account_email)}.*.log"; + + if (!logsLock.Contains(line)) + { + logsLock += $"{DateTime.Now}: {line}\n"; + File.WriteAllText(logFile, logsLock); + } + } + #endregion + + #region countlock_day + static int countlock_day(IMemoryCache memoryCache, bool update, string account_email) + { + var lockhour = memoryCache.GetOrCreate($"Accsdb:lock_day:{account_email}:{DateTime.Now.Day}", entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); + return new ConcurrentDictionary(); + }); + + if (update) + lockhour.TryAdd(DateTime.Now.Hour, 0); + + return lockhour.Count; + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/AlwaysRjson.cs b/Lampac/Engine/Middlewares/AlwaysRjson.cs new file mode 100644 index 0000000..030f48c --- /dev/null +++ b/Lampac/Engine/Middlewares/AlwaysRjson.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.WebUtilities; +using Shared; +using System; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class AlwaysRjson + { + private readonly RequestDelegate _next; + + public AlwaysRjson(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext context) + { + if (!AppInit.conf.always_rjson) + return _next(context); + + var builder = new QueryBuilder(); + + foreach (var kv in QueryHelpers.ParseQuery(context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty)) + { + if (string.Equals(kv.Key, "rjson", StringComparison.OrdinalIgnoreCase)) + continue; + + foreach (var value in kv.Value) + builder.Add(kv.Key, value); + } + + builder.Add("rjson", "true"); + + context.Request.QueryString = builder.ToQueryString(); + + return _next(context); + } + } +} diff --git a/Lampac/Engine/Middlewares/AnonymousRequest.cs b/Lampac/Engine/Middlewares/AnonymousRequest.cs new file mode 100644 index 0000000..dee358c --- /dev/null +++ b/Lampac/Engine/Middlewares/AnonymousRequest.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Shared.Models; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class AnonymousRequest + { + private readonly RequestDelegate _next; + public AnonymousRequest(RequestDelegate next) + { + _next = next; + } + + static readonly Regex rexProxy = new Regex("^/(proxy-dash|cub|ts|kit|bind)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex rexJs = new Regex("^/[a-zA-Z\\-]+\\.js", RegexOptions.Compiled); + + public Task Invoke(HttpContext httpContext) + { + var requestInfo = httpContext.Features.Get(); + + var endpoint = httpContext.GetEndpoint(); + if (endpoint != null && endpoint.Metadata.GetMetadata() != null) + requestInfo.IsAnonymousRequest = true; + + if (httpContext.Request.Path.Value == "/" || httpContext.Request.Path.Value == "/favicon.ico") + requestInfo.IsAnonymousRequest = true; + + if (httpContext.Request.Path.Value == "/.well-known/appspecific/com.chrome.devtools.json") + requestInfo.IsAnonymousRequest = true; + + if (rexProxy.IsMatch(httpContext.Request.Path.Value)) + requestInfo.IsAnonymousRequest = true; + + if (rexJs.IsMatch(httpContext.Request.Path.Value)) + requestInfo.IsAnonymousRequest = true; + + return _next(httpContext); + } + } +} diff --git a/Lampac/Engine/Middlewares/BaseMod.cs b/Lampac/Engine/Middlewares/BaseMod.cs new file mode 100644 index 0000000..c992f8b --- /dev/null +++ b/Lampac/Engine/Middlewares/BaseMod.cs @@ -0,0 +1,166 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class BaseMod + { + private readonly RequestDelegate _next; + + public BaseMod(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext context) + { + if (!HttpMethods.IsGet(context.Request.Method) && + !HttpMethods.IsPost(context.Request.Method) && + !HttpMethods.IsOptions(context.Request.Method)) + return Task.CompletedTask; + + if (!IsValidPath(context.Request.Path.Value)) + { + context.Response.StatusCode = 400; + return context.Response.WriteAsync("400 Bad Request", context.RequestAborted); + } + + if (Program.RuntimeCve2025_55315) + { + if (Regex.IsMatch(context.Request.Path.Value, "^/(ffprobe|transcoding|dlna|admin)", RegexOptions.IgnoreCase)) + { + string ip = context.Connection.RemoteIpAddress.ToString(); + if (!Shared.Engine.Utilities.IPNetwork.IsLocalIp(ip)) + { + context.Response.StatusCode = 400; + return context.Response.WriteAsync("Please update dotnet\nhttps://github.com/dotnet/core/blob/main/release-notes/9.0/9.0.12/9.0.113.md", context.RequestAborted); + } + } + } + + var builder = new QueryBuilder(); + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + var sbQuery = new StringBuilder(32); + + foreach (var q in context.Request.Query) + { + if (IsValidQueryName(q.Key)) + { + string val = ValidQueryValue(sbQuery, q.Key, q.Value); + + if (dict.TryAdd(q.Key, val)) + builder.Add(q.Key, val); + } + } + + context.Request.QueryString = builder.ToQueryString(); + context.Request.Query = new QueryCollection(dict); + + return _next(context); + } + + + #region IsValid + static bool IsValidPath(ReadOnlySpan path) + { + if (path.IsEmpty) + return false; + + foreach (char ch in path) + { + if ( + ch == '/' || ch == '-' || ch == '.' || ch == '_' || + ch == ':' || ch == '+' || ch == '=' || + (ch >= 'A' && ch <= 'Z') || + (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') + ) + { + continue; + } + + return false; + } + + return true; + } + + static bool IsValidQueryName(ReadOnlySpan path) + { + if (path.IsEmpty) + return false; + + foreach (char ch in path) + { + if ( + ch == '-' || ch == '_' || + (ch >= 'A' && ch <= 'Z') || + (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') || + ch == '.' // tmdb + ) + { + continue; + } + + return false; + } + + return true; + } + + static string ValidQueryValue(StringBuilder sb, string name, StringValues values) + { + if (values.Count == 0) + return string.Empty; + + string value = values[0]; + + if (string.IsNullOrEmpty(value)) + return string.Empty; + + sb.Clear(); + + foreach (char ch in value) + { + if ( + ch == '/' || ch == ':' || ch == '?' || ch == '&' || ch == '=' || ch == '.' || // ссылки + ch == '-' || ch == '_' || ch == ' ' || ch == ',' || // base + (ch >= '0' && ch <= '9') || + ch == '@' || // email + ch == '+' || // aes + ch == '*' || // merchant + ch == '|' || // tmdb + char.IsLetter(ch) // ← любые буквы Unicode + ) + { + sb.Append(ch); + continue; + } + + if (name is "search" or "query" or "title" or "original_title" or "t") + { + if ( + char.IsDigit(ch) || // ← символ цифрой Unicode + ch == '\'' || ch == '!' || ch == ',' || ch == '+' || ch == '~' || ch == '"' || ch == ';' || + ch == '(' || ch == ')' || ch == '[' || ch == ']' || ch == '{' || ch == '}' || ch == '«' || ch == '»' || ch == '“' || ch == '”' || + ch == '$' || ch == '%' || ch == '^' || ch == '#' || ch == '×' + ) + { + sb.Append(ch); + continue; + } + } + } + + return sb.ToString(); + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/Extensions.cs b/Lampac/Engine/Middlewares/Extensions.cs new file mode 100644 index 0000000..4ca85d6 --- /dev/null +++ b/Lampac/Engine/Middlewares/Extensions.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Builder; + +namespace Lampac.Engine.Middlewares +{ + public static class Extensions + { + public static IApplicationBuilder UseBaseMod(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseWAF(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseModHeaders(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseRequestStatistics(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseOverrideResponse(this IApplicationBuilder builder, bool first) + { + return builder.UseMiddleware(first); + } + + public static IApplicationBuilder UseRequestInfo(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseAnonymousRequest(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseAlwaysRjson(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseAccsdb(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseProxyAPI(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseProxyIMG(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseProxyCub(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseProxyTmdb(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseModule(this IApplicationBuilder builder, bool first) + { + return builder.UseMiddleware(first); + } + + public static IApplicationBuilder UseStaticache(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/Lampac/Engine/Middlewares/ModHeaders.cs b/Lampac/Engine/Middlewares/ModHeaders.cs new file mode 100644 index 0000000..99c0764 --- /dev/null +++ b/Lampac/Engine/Middlewares/ModHeaders.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class ModHeaders + { + private readonly RequestDelegate _next; + public ModHeaders(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext httpContext) + { + if (httpContext.Request.Path.Value.StartsWith("/cors/check", StringComparison.OrdinalIgnoreCase)) + return Task.CompletedTask; + + httpContext.Response.Headers["Access-Control-Allow-Credentials"] = "true"; + httpContext.Response.Headers["Access-Control-Allow-Private-Network"] = "true"; + httpContext.Response.Headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS"; + + if (GetAllowHeaders(httpContext, out HashSet allowHeadersSet)) + httpContext.Response.Headers["Access-Control-Allow-Headers"] = string.Join(", ", allowHeadersSet); + else + httpContext.Response.Headers["Access-Control-Allow-Headers"] = stringAllowHeaders; + + if (httpContext.Request.Headers.TryGetValue("origin", out var origin)) + httpContext.Response.Headers["Access-Control-Allow-Origin"] = GetOrigin(origin); + else if (httpContext.Request.Headers.TryGetValue("referer", out var referer)) + httpContext.Response.Headers["Access-Control-Allow-Origin"] = GetOrigin(referer); + else + httpContext.Response.Headers["Access-Control-Allow-Origin"] = "*"; + + if (Regex.IsMatch(httpContext.Request.Path.Value, "^/(lampainit|sisi|lite|online|tmdbproxy|cubproxy|tracks|transcoding|dlna|timecode|bookmark|catalog|sync|backup|ts|invc-ws)\\.js", RegexOptions.IgnoreCase) || + Regex.IsMatch(httpContext.Request.Path.Value, "^/(on/|(lite|online|sisi|timecode|bookmark|sync|tmdbproxy|dlna|ts|tracks|transcoding|backup|catalog|invc-ws)/js/)", RegexOptions.IgnoreCase)) + { + httpContext.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; // HTTP 1.1. + httpContext.Response.Headers["Pragma"] = "no-cache"; // HTTP 1.0. + httpContext.Response.Headers["Expires"] = "0"; // Proxies. + } + + if (HttpMethods.IsOptions(httpContext.Request.Method)) + return Task.CompletedTask; + + return _next(httpContext); + } + + + static readonly string stringAllowHeaders = "Authorization, Token, Profile, X-Kit-AesGcm, Content-Type, X-Signalr-User-Agent, X-Requested-With"; + + static readonly HashSet hashAllowHeaders = new HashSet( + [ + "Authorization", "Token", "Profile", "X-Kit-AesGcm", + "Content-Type", "X-Signalr-User-Agent", "X-Requested-With" + ], StringComparer.OrdinalIgnoreCase); + + + static bool GetAllowHeaders(HttpContext httpContext, out HashSet headersSet) + { + if (httpContext.Request.Headers.TryGetValue("Access-Control-Request-Headers", out var requestedHeaders)) + { + headersSet = [.. hashAllowHeaders]; + + foreach (string header in requestedHeaders.ToString().Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + if (!string.IsNullOrWhiteSpace(header)) + headersSet.Add(header.Trim()); + } + + return true; + } + + headersSet = null; + return false; + } + + static string GetOrigin(string url) + { + if (string.IsNullOrEmpty(url)) + return string.Empty; + + int scheme = url.IndexOf("://", StringComparison.Ordinal); + if (scheme <= 0) + return url; + + int start = scheme + 3; + int slash = url.IndexOf('/', start); + if (slash < 0) + return url; // уже origin + + return url.Substring(0, slash); + } + } +} diff --git a/Lampac/Engine/Middlewares/Module.cs b/Lampac/Engine/Middlewares/Module.cs new file mode 100644 index 0000000..38a0d3a --- /dev/null +++ b/Lampac/Engine/Middlewares/Module.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Events; +using Shared.Models.Module.Entrys; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class Module + { + private readonly RequestDelegate _next; + IMemoryCache memoryCache; + private readonly bool first; + + public Module(RequestDelegate next, IMemoryCache mem, bool first) + { + _next = next; + memoryCache = mem; + this.first = first; + } + + async public Task InvokeAsync(HttpContext httpContext) + { + #region modules + MiddlewaresModuleEntry.EnsureCache(); + + if (MiddlewaresModuleEntry.middlewareModulesCache != null && MiddlewaresModuleEntry.middlewareModulesCache.Count > 0) + { + foreach (var entry in MiddlewaresModuleEntry.middlewareModulesCache) + { + var mod = entry.mod; + + try + { + if (first && (mod.version == 0 || mod.version == 1)) + continue; + + if (mod.version >= 2) + { + if (entry.Invoke != null) + { + bool next = entry.Invoke(first, httpContext, memoryCache); + if (!next) + return; + } + + if (entry.InvokeAsync != null) + { + bool next = await entry.InvokeAsync(first, httpContext, memoryCache); + if (!next) + return; + } + } + else + { + if (entry.InvokeV1 != null) + { + bool next = entry.InvokeV1(httpContext, memoryCache); + if (!next) + return; + } + + if (entry.InvokeAsyncV1 != null) + { + bool next = await entry.InvokeAsyncV1(httpContext, memoryCache); + if (!next) + return; + } + } + } + catch { } + } + } + #endregion + + if (InvkEvent.IsMiddleware(first)) + { + var rqinfo = httpContext.Features.Get(); + bool next = await InvkEvent.Middleware(first, new EventMiddleware(rqinfo, httpContext.Request, httpContext, IHybridCache.Get(rqinfo), memoryCache)); + if (!next) + return; + } + + await _next(httpContext); + } + } +} diff --git a/Lampac/Engine/Middlewares/OverrideResponse.cs b/Lampac/Engine/Middlewares/OverrideResponse.cs new file mode 100644 index 0000000..e75777a --- /dev/null +++ b/Lampac/Engine/Middlewares/OverrideResponse.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.CodeAnalysis.Scripting; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.CSharpGlobals; +using System; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class OverrideResponse + { + private readonly RequestDelegate _next; + private readonly bool first; + public OverrideResponse(RequestDelegate next, bool first) + { + _next = next; + this.first = first; + } + + public Task Invoke(HttpContext httpContext) + { + if (AppInit.conf.overrideResponse != null && AppInit.conf.overrideResponse.Count > 0) + { + var requestInfo = httpContext.Features.Get(); + if (requestInfo.IsLocalRequest) + return _next(httpContext); + + string url = httpContext.Request.Path.Value + httpContext.Request.QueryString.Value; + + foreach (var over in AppInit.conf.overrideResponse) + { + if (over.firstEndpoint != first) + continue; + + if (Regex.IsMatch(url, over.pattern, RegexOptions.IgnoreCase)) + { + switch (over.action) + { + case "html": + { + httpContext.Response.ContentType = over.type; + return httpContext.Response.WriteAsync(over.val.Replace("{localhost}", AppInit.Host(httpContext)), httpContext.RequestAborted); + } + case "file": + { + httpContext.Response.ContentType = over.type; + + if (string.IsNullOrEmpty(over.val) || !File.Exists(over.val)) + { + httpContext.Response.StatusCode = 404; + return Task.CompletedTask; + } + + if (Regex.IsMatch(over.val, "\\.(html|txt|css|js|json|xml)$", RegexOptions.IgnoreCase)) + { + string val = FileCache.ReadAllText(over.val); + return httpContext.Response.WriteAsync(val.Replace("{localhost}", AppInit.Host(httpContext)), httpContext.RequestAborted); + } + else + { + return httpContext.Response.SendFileAsync(over.val); + } + } + case "redirect": + { + httpContext.Response.Redirect(over.val); + return Task.CompletedTask; + } + case "eval": + { + var options = ScriptOptions.Default + .AddReferences(typeof(Console).Assembly).AddImports("System") + .AddReferences(typeof(Regex).Assembly).AddImports("System.Text.RegularExpressions"); + + bool _next = CSharpEval.BaseExecute(over.val, new OverrideResponseGlobals(url, httpContext.Request, requestInfo), options); + if (!_next) + return Task.CompletedTask; + break; + } + default: + break; + } + } + } + } + + return _next(httpContext); + } + } +} diff --git a/Lampac/Engine/Middlewares/ProxyAPI.cs b/Lampac/Engine/Middlewares/ProxyAPI.cs new file mode 100644 index 0000000..66e6b69 --- /dev/null +++ b/Lampac/Engine/Middlewares/ProxyAPI.cs @@ -0,0 +1,848 @@ +using Microsoft.AspNetCore.Http; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.Proxy; +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class ProxyAPI + { + static readonly HttpClientHandler baseHandler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.None, + AllowAutoRedirect = false, + UseProxy = false, + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true + }; + + #region ProxyAPI + static readonly FileSystemWatcher fileWatcher; + + static readonly ConcurrentDictionary cacheFiles = new(); + + public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count; + + static ProxyAPI() + { + Directory.CreateDirectory("cache/hls"); + + foreach (string path in Directory.EnumerateFiles("cache/hls", "*")) + { + using (var handle = File.OpenHandle(path)) + cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle)); + } + + fileWatcher = new FileSystemWatcher + { + Path = "cache/hls", + NotifyFilter = NotifyFilters.FileName, + EnableRaisingEvents = true + }; + + fileWatcher.Deleted += (s, e) => { cacheFiles.TryRemove(e.Name, out _); }; + } + + public ProxyAPI(RequestDelegate next) { } + #endregion + + async public Task InvokeAsync(HttpContext httpContext) + { + var init = AppInit.conf.serverproxy; + var requestInfo = httpContext.Features.Get(); + + string servPath = httpContext.Request.Path.Value.Replace("/proxy/", "", StringComparison.OrdinalIgnoreCase).Replace("/proxy-dash/", "", StringComparison.OrdinalIgnoreCase); + string servUri = servPath + httpContext.Request.QueryString.Value; + + #region tmdb proxy + if (servUri.Contains(".themoviedb.org", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.Redirect($"/tmdb/api/{Regex.Match(servUri.Replace("://", ":/_/").Replace("//", "/").Replace(":/_/", "://"), "https?://[^/]+/(.*)").Groups[1].Value}"); + return; + } + else if (servUri.Contains(".tmdb.org", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.Redirect($"/tmdb/img/{Regex.Match(servUri.Replace("://", ":/_/").Replace("//", "/").Replace(":/_/", "://"), "https?://[^/]+/(.*)").Groups[1].Value}"); + return; + } + #endregion + + #region decryptLink + var decryptLink = ProxyLink.Decrypt(httpContext.Request.Path.Value.StartsWith("/proxy-dash/", StringComparison.OrdinalIgnoreCase) ? servPath.Split("/")[0] : servPath, requestInfo.IP); + + if (init.encrypt || decryptLink?.uri != null || httpContext.Request.Path.Value.StartsWith("/proxy-dash/", StringComparison.OrdinalIgnoreCase)) + { + servUri = decryptLink?.uri; + } + else + { + if (!init.enable) + { + httpContext.Response.StatusCode = 403; + return; + } + } + + if (string.IsNullOrWhiteSpace(servUri) || !servUri.StartsWith("http")) + { + httpContext.Response.StatusCode = 404; + return; + } + + if (decryptLink == null) + decryptLink = new ProxyLinkModel(requestInfo.IP, null, null, servUri); + #endregion + + if (init.showOrigUri) + { + //Console.WriteLine("PX-Orig: " + decryptLink.uri); + httpContext.Response.Headers["PX-Orig"] = decryptLink.uri; + } + + #region proxyHandler + HttpClientHandler proxyHandler = null; + + if (decryptLink.proxy != null) + { + proxyHandler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.None, + AllowAutoRedirect = false + }; + + proxyHandler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + proxyHandler.UseProxy = true; + proxyHandler.Proxy = decryptLink.proxy; + } + #endregion + + #region cacheFiles + (string uriKey, string contentType) cacheStream = InvkEvent.IsProxyApiCacheStream() + ? InvkEvent.ProxyApiCacheStream(httpContext, decryptLink) + : default; + + if (cacheStream.uriKey != null && init.showOrigUri) + httpContext.Response.Headers["PX-CacheStream"] = cacheStream.uriKey; + + if (cacheStream.uriKey != null) + { + string md5key = CrypTo.md5(cacheStream.uriKey); + + if (cacheFiles.ContainsKey(md5key)) + { + httpContext.Response.Headers["PX-Cache"] = "HIT"; + httpContext.Response.Headers["accept-ranges"] = "bytes"; + httpContext.Response.ContentType = cacheStream.contentType ?? "application/octet-stream"; + + long cacheLength = cacheFiles[md5key]; + string cachePath = $"cache/hls/{md5key}"; + + if (RangeHeaderValue.TryParse(httpContext.Request.Headers["Range"], out var range)) + { + var rangeItem = range.Ranges.FirstOrDefault(); + if (rangeItem != null) + { + long start = rangeItem.From ?? 0; + long end = rangeItem.To ?? (cacheLength - 1); + + if (start >= cacheLength) + { + httpContext.Response.StatusCode = StatusCodes.Status416RangeNotSatisfiable; + httpContext.Response.Headers["content-range"] = $"bytes */{cacheLength}"; + return; + } + + if (end >= cacheLength) + end = cacheLength - 1; + + long length = end - start + 1; + + httpContext.Response.StatusCode = StatusCodes.Status206PartialContent; + httpContext.Response.Headers["content-range"] = $"bytes {start}-{end}/{cacheLength}"; + + if (init.responseContentLength) + httpContext.Response.ContentLength = length; + + await httpContext.Response.SendFileAsync(cachePath, start, length, httpContext.RequestAborted).ConfigureAwait(false); + return; + } + } + + if (init.responseContentLength) + httpContext.Response.ContentLength = cacheLength; + + await httpContext.Response.SendFileAsync(cachePath, httpContext.RequestAborted).ConfigureAwait(false); + return; + } + } + #endregion + + if (httpContext.Request.Path.Value.StartsWith("/proxy-dash/", StringComparison.OrdinalIgnoreCase)) + { + #region DASH + var uri = new Uri($"{servUri}{Regex.Replace(httpContext.Request.Path.Value, "^/[^/]+/[^/]+/", "", RegexOptions.IgnoreCase)}{httpContext.Request.QueryString.Value}"); + + var client = FrendlyHttp.MessageClient("proxy", proxyHandler ?? baseHandler); + + using (var request = CreateProxyHttpRequest(decryptLink.plugin, httpContext, decryptLink.headers, uri, true)) + { + if (InvkEvent.IsProxyApiCreateHttpRequest()) + await InvkEvent.ProxyApiCreateHttpRequest(decryptLink.plugin, httpContext.Request, decryptLink.headers, uri, true, request).ConfigureAwait(false); + + using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted)) + { + ctsHttp.CancelAfter(TimeSpan.FromSeconds(30)); + + using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctsHttp.Token).ConfigureAwait(false)) + { + httpContext.Response.Headers["PX-Cache"] = "BYPASS"; + await CopyProxyHttpResponse(httpContext, response, cacheStream.uriKey).ConfigureAwait(false); + } + } + } + #endregion + } + else + { + #region Video OR + if (servUri.Contains(" or ")) + { + string[] links = servUri.Split(" or "); + servUri = links[0].Trim(); + + try + { + var hdlr = new HttpClientHandler() + { + AllowAutoRedirect = true + }; + + hdlr.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + + if (decryptLink.proxy != null) + { + hdlr.UseProxy = true; + hdlr.Proxy = decryptLink.proxy; + } + else { hdlr.UseProxy = false; } + + var clientor = FrendlyHttp.MessageClient("base", hdlr); + + using (var requestor = CreateProxyHttpRequest(decryptLink.plugin, httpContext, decryptLink.headers, new Uri(servUri), true)) + { + if (InvkEvent.IsProxyApiCreateHttpRequest()) + await InvkEvent.ProxyApiCreateHttpRequest(decryptLink.plugin, httpContext.Request, decryptLink.headers, new Uri(servUri), true, requestor).ConfigureAwait(false); + + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(7))) + { + using (var response = await clientor.SendAsync(requestor, HttpCompletionOption.ResponseHeadersRead, cts.Token).ConfigureAwait(false)) + { + if ((int)response.StatusCode is 200 or 206) { } + else + servUri = links[1].Trim(); + } + } + } + } + catch + { + servUri = links[1].Trim(); + } + + servUri = servUri.Split(" ")[0].Trim(); + decryptLink.uri = servUri; + + if (init.showOrigUri) + httpContext.Response.Headers["PX-Set-Orig"] = decryptLink.uri; + } + #endregion + + var client = FrendlyHttp.MessageClient("proxy", proxyHandler ?? baseHandler); + + bool ismedia = Regex.IsMatch(httpContext.Request.Path.Value, "\\.(m3u|ts|m4s|mp4|mkv|aacp|srt|vtt)", RegexOptions.IgnoreCase); + using (var request = CreateProxyHttpRequest(decryptLink.plugin, httpContext, decryptLink.headers, new Uri(servUri), ismedia)) + { + if (InvkEvent.IsProxyApiCreateHttpRequest()) + await InvkEvent.ProxyApiCreateHttpRequest(decryptLink.plugin, httpContext.Request, decryptLink.headers, new Uri(servUri), ismedia, request).ConfigureAwait(false); + + using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted)) + { + ctsHttp.CancelAfter(TimeSpan.FromSeconds(30)); + + using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctsHttp.Token).ConfigureAwait(false)) + { + if ((int)response.StatusCode is 301 or 302 or 303 or 0 || response.Headers.Location != null) + { + httpContext.Response.Redirect(validArgs($"{AppInit.Host(httpContext)}/proxy/{ProxyLink.Encrypt(response.Headers.Location.AbsoluteUri, decryptLink)}", httpContext)); + return; + } + + IEnumerable _contentType = null; + if (response.Content?.Headers != null) + response.Content.Headers.TryGetValues("Content-Type", out _contentType); + + string contentType = _contentType?.FirstOrDefault()?.ToLower(); + + bool ists = httpContext.Request.Path.Value.EndsWith(".ts", StringComparison.OrdinalIgnoreCase) || httpContext.Request.Path.Value.EndsWith(".m4s", StringComparison.OrdinalIgnoreCase); + + if (!ists && (httpContext.Request.Path.Value.Contains(".m3u", StringComparison.OrdinalIgnoreCase) || (contentType != null && contentType is "application/x-mpegurl" or "application/vnd.apple.mpegurl" or "text/plain"))) + { + #region m3u8/txt + using (HttpContent content = response.Content) + { + if (response.StatusCode == HttpStatusCode.OK || + response.StatusCode == HttpStatusCode.PartialContent || + response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) + { + if (response.Content?.Headers?.ContentLength > init.maxlength_m3u) + { + httpContext.Response.StatusCode = 503; + httpContext.Response.ContentType = "text/plain"; + await httpContext.Response.WriteAsync("bigfile", ctsHttp.Token).ConfigureAwait(false); + return; + } + + string m3u8 = await content.ReadAsStringAsync(ctsHttp.Token).ConfigureAwait(false); + if (m3u8 == null) + { + httpContext.Response.StatusCode = 503; + await httpContext.Response.WriteAsync("error array m3u8", ctsHttp.Token).ConfigureAwait(false); + return; + } + + byte[] hlsArray = editm3u(m3u8, httpContext, decryptLink); + + httpContext.Response.ContentType = contentType ?? "application/vnd.apple.mpegurl"; + httpContext.Response.StatusCode = (int)response.StatusCode; + + if (response.Headers.AcceptRanges != null) + httpContext.Response.Headers["accept-ranges"] = "bytes"; + + if (httpContext.Response.StatusCode is 206 or 416) + { + var contentRange = response.Content?.Headers?.ContentRange; + if (contentRange != null) + { + httpContext.Response.Headers["content-range"] = contentRange.ToString(); + } + else + { + if (httpContext.Response.StatusCode == 206) + httpContext.Response.Headers["content-range"] = $"bytes 0-{hlsArray.Length - 1}/{hlsArray.Length}"; + + if (httpContext.Response.StatusCode == 416) + httpContext.Response.Headers["content-range"] = $"bytes */{hlsArray.Length}"; + } + } + else + { + if (init.responseContentLength && !AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType)) + httpContext.Response.ContentLength = hlsArray.Length; + } + + await httpContext.Response.Body.WriteAsync(hlsArray, ctsHttp.Token).ConfigureAwait(false); + } + else + { + // проксируем ошибку + await CopyProxyHttpResponse(httpContext, response, null).ConfigureAwait(false); + } + } + #endregion + } + else if (httpContext.Request.Path.Value.Contains(".mpd", StringComparison.OrdinalIgnoreCase) || (contentType != null && contentType == "application/dash+xml")) + { + #region dash + using (HttpContent content = response.Content) + { + if (response.StatusCode == HttpStatusCode.OK || + response.StatusCode == HttpStatusCode.PartialContent || + response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) + { + if (response.Content?.Headers?.ContentLength > init.maxlength_m3u) + { + httpContext.Response.StatusCode = 503; + httpContext.Response.ContentType = "text/plain"; + await httpContext.Response.WriteAsync("bigfile", ctsHttp.Token).ConfigureAwait(false); + return; + } + + string mpd = await content.ReadAsStringAsync(ctsHttp.Token).ConfigureAwait(false); + if (mpd == null) + { + httpContext.Response.StatusCode = 503; + await httpContext.Response.WriteAsync("error array mpd", ctsHttp.Token).ConfigureAwait(false); + return; + } + + var m = Regex.Match(mpd, "([^<]+)"); + while (m.Success) + { + string baseURL = m.Groups[1].Value; + mpd = Regex.Replace(mpd, baseURL, $"{AppInit.Host(httpContext)}/proxy-dash/{ProxyLink.Encrypt(baseURL, decryptLink, forceMd5: true)}/"); + m = m.NextMatch(); + } + + byte[] mpdArray = Encoding.UTF8.GetBytes(mpd); + + httpContext.Response.ContentType = contentType ?? "application/dash+xml"; + httpContext.Response.StatusCode = (int)response.StatusCode; + + if (response.Headers.AcceptRanges != null) + httpContext.Response.Headers["accept-ranges"] = "bytes"; + + if (httpContext.Response.StatusCode is 206 or 416) + { + var contentRange = response.Content.Headers.ContentRange; + if (contentRange != null) + { + httpContext.Response.Headers["content-range"] = contentRange.ToString(); + } + else + { + if (httpContext.Response.StatusCode == 206) + httpContext.Response.Headers["content-range"] = $"bytes 0-{mpdArray.Length - 1}/{mpdArray.Length}"; + + if (httpContext.Response.StatusCode == 416) + httpContext.Response.Headers["content-range"] = $"bytes */{mpdArray.Length}"; + } + } + else + { + if (init.responseContentLength && !AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType)) + httpContext.Response.ContentLength = mpdArray.Length; + } + + await httpContext.Response.Body.WriteAsync(mpdArray, ctsHttp.Token).ConfigureAwait(false); + } + else + { + // проксируем ошибку + await CopyProxyHttpResponse(httpContext, response, null).ConfigureAwait(false); + } + } + #endregion + } + else + { + httpContext.Response.Headers["PX-Cache"] = cacheStream.uriKey != null ? "MISS" : "BYPASS"; + await CopyProxyHttpResponse(httpContext, response, cacheStream.uriKey).ConfigureAwait(false); + } + } + } + } + } + } + + + #region validArgs + static string validArgs(string uri, HttpContext httpContext) + { + if (AppInit.conf.accsdb.enable && !AppInit.conf.serverproxy.encrypt) + return AccsDbInvk.Args(uri, httpContext); + + return uri; + } + #endregion + + #region editm3u + static byte[] editm3u(string _m3u8, HttpContext httpContext, ProxyLinkModel decryptLink) + { + string proxyhost = $"{AppInit.Host(httpContext)}/proxy"; + string m3u8 = Regex.Replace(_m3u8, "(https?://[^\n\r\"\\# ]+)", m => + { + return validArgs($"{proxyhost}/{ProxyLink.Encrypt(m.Groups[1].Value, decryptLink)}", httpContext); + }); + + string hlshost = Regex.Match(decryptLink.uri, "(https?://[^/]+)/").Groups[1].Value; + string hlspatch = Regex.Match(decryptLink.uri, "(https?://[^\n\r]+/)([^/]+)$").Groups[1].Value; + if (string.IsNullOrEmpty(hlspatch) && decryptLink.uri.EndsWith("/")) + hlspatch = decryptLink.uri; + + m3u8 = Regex.Replace(m3u8, "([\n\r])([^\n\r]+)", m => + { + string uri = m.Groups[2].Value; + + if (uri.Contains("#") || uri.Contains("\"") || uri.StartsWith("http")) + return m.Groups[0].Value; + + if (uri.StartsWith("//")) + { + uri = "https:" + uri; + } + else if (uri.StartsWith("/")) + { + uri = hlshost + uri; + } + else if (uri.StartsWith("./")) + { + uri = hlspatch + uri.Substring(2); + } + else + { + uri = hlspatch + uri; + } + + return m.Groups[1].Value + validArgs($"{proxyhost}/{ProxyLink.Encrypt(uri, decryptLink)}", httpContext); + }); + + m3u8 = Regex.Replace(m3u8, "(URI=\")([^\"]+)", m => + { + string uri = m.Groups[2].Value; + + if (uri.Contains("\"") || uri.StartsWith("http")) + return m.Groups[0].Value; + + if (uri.StartsWith("//")) + { + uri = "https:" + uri; + } + else if (uri.StartsWith("/")) + { + uri = hlshost + uri; + } + else if (uri.StartsWith("./")) + { + uri = hlspatch + uri.Substring(2); + } + else + { + uri = hlspatch + uri; + } + + return m.Groups[1].Value + validArgs($"{proxyhost}/{ProxyLink.Encrypt(uri, decryptLink)}", httpContext); + }); + + return Encoding.UTF8.GetBytes(m3u8); + } + #endregion + + + #region CreateProxyHttpRequest + static HttpRequestMessage CreateProxyHttpRequest(string plugin, HttpContext context, List headers, Uri uri, bool ismedia) + { + var request = context.Request; + + var requestMessage = new HttpRequestMessage(); + + var requestMethod = request.Method; + if (HttpMethods.IsPost(requestMethod)) + { + var streamContent = new StreamContent(request.Body); + requestMessage.Content = streamContent; + } + + #region Headers + { + var addHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["accept"] = ["*/*"], + ["accept-language"] = ["ru-RU,ru;q=0.9,uk-UA;q=0.8,uk;q=0.7,en-US;q=0.6,en;q=0.5"] + }; + + if (headers != null && headers.Count > 0) + { + foreach (var h in headers) + addHeaders[h.name] = [h.val]; + } + + if (ismedia) + { + if (request.Headers.TryGetValue("range", out var range)) + addHeaders["range"] = range.ToArray(); + } + else + { + foreach (var header in request.Headers) + { + string key = header.Key; + + if (key.Equals("host", StringComparison.OrdinalIgnoreCase) || + key.Equals("origin", StringComparison.OrdinalIgnoreCase) || + key.Equals("user-agent", StringComparison.OrdinalIgnoreCase) || + key.Equals("referer", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) || + key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.StartsWith("x-")) + continue; + + if (key == "range") + { + addHeaders[key] = header.Value.ToArray(); + continue; + } + + addHeaders.TryAdd(key, header.Value.ToArray()); + } + } + + foreach (var h in Http.defaultFullHeaders) + addHeaders[h.Key] = [h.Value]; + + foreach (var h in Http.NormalizeHeaders(addHeaders)) + { + if (!requestMessage.Headers.TryAddWithoutValidation(h.Key, h.Value)) + { + if (requestMessage.Content?.Headers != null) + requestMessage.Content.Headers.TryAddWithoutValidation(h.Key, h.Value); + } + } + } + #endregion + + requestMessage.Headers.Host = uri.Authority; + requestMessage.RequestUri = uri; + requestMessage.Method = new HttpMethod(request.Method); + + //requestMessage.Version = new Version(2, 0); + //Console.WriteLine(JsonConvert.SerializeObject(requestMessage.Headers, Formatting.Indented)); + + return requestMessage; + } + #endregion + + #region CopyProxyHttpResponse + async Task CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage, string uriKeyFileCache) + { + var response = context.Response; + response.StatusCode = (int)responseMessage.StatusCode; + + #region responseContentLength + if (AppInit.conf.serverproxy.responseContentLength && responseMessage.Content?.Headers?.ContentLength > 0) + { + IEnumerable contentType = null; + if (responseMessage.Content?.Headers != null) + responseMessage.Content.Headers.TryGetValues("Content-Type", out contentType); + + string type = contentType?.FirstOrDefault()?.ToLowerInvariant(); + + if (string.IsNullOrEmpty(type) || !AppInit.CompressionMimeTypes.Contains(type)) + response.ContentLength = responseMessage.Content.Headers.ContentLength; + } + #endregion + + #region UpdateHeaders + void UpdateHeaders(HttpHeaders headers) + { + foreach (var header in headers) + { + string key = header.Key; + + if (key.Equals("server", StringComparison.OrdinalIgnoreCase) || + key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase) || + key.Equals("etag", StringComparison.OrdinalIgnoreCase) || + key.Equals("connection", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-security-policy", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-length", StringComparison.OrdinalIgnoreCase) || + key.Equals("set-cookie", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.StartsWith("x-", StringComparison.OrdinalIgnoreCase) || + key.StartsWith("alt-", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.StartsWith("access-control", StringComparison.OrdinalIgnoreCase)) + continue; + + var values = header.Value; + + using (var e = values.GetEnumerator()) + { + if (!e.MoveNext()) + continue; + + string first = e.Current; + + response.Headers[key] = e.MoveNext() + ? string.Join("; ", values) + : first; + } + } + } + #endregion + + UpdateHeaders(responseMessage.Headers); + UpdateHeaders(responseMessage.Content.Headers); + + using (var responseStream = await responseMessage.Content.ReadAsStreamAsync(context.RequestAborted).ConfigureAwait(false)) + { + if (response.Body == null) + throw new ArgumentNullException("destination"); + + if (!responseStream.CanRead && !responseStream.CanWrite) + throw new ObjectDisposedException("ObjectDisposed_StreamClosed"); + + if (!response.Body.CanRead && !response.Body.CanWrite) + throw new ObjectDisposedException("ObjectDisposed_StreamClosed"); + + if (!responseStream.CanRead) + throw new NotSupportedException("NotSupported_UnreadableStream"); + + if (!response.Body.CanWrite) + throw new NotSupportedException("NotSupported_UnwritableStream"); + + var buffering = AppInit.conf.serverproxy?.buffering; + + if (buffering?.enable == true && + ((!string.IsNullOrEmpty(buffering.pattern) && Regex.IsMatch(context.Request.Path.Value, buffering.pattern, RegexOptions.IgnoreCase)) || + context.Request.Path.Value.EndsWith(".mp4") || context.Request.Path.Value.EndsWith(".mkv") || responseMessage.Content?.Headers?.ContentLength > 40_000000)) + { + #region buffering + var channel = Channel.CreateBounded<(byte[] Buffer, int Length)>(new BoundedChannelOptions(capacity: buffering.length) + { + FullMode = BoundedChannelFullMode.Wait, + SingleWriter = true, + SingleReader = true + }); + + var readTask = Task.Factory.StartNew(async () => + { + try + { + while (!context.RequestAborted.IsCancellationRequested) + { + byte[] chunkBuffer = ArrayPool.Shared.Rent(PoolInvk.Rent(buffering.rent)); + + try + { + int bytesRead = await responseStream.ReadAsync(chunkBuffer, 0, chunkBuffer.Length, context.RequestAborted); + + if (bytesRead == 0) + { + ArrayPool.Shared.Return(chunkBuffer); + break; + } + + await channel.Writer.WriteAsync((chunkBuffer, bytesRead), context.RequestAborted); + } + catch + { + ArrayPool.Shared.Return(chunkBuffer); + break; + } + } + } + finally + { + channel.Writer.Complete(); + } + }, + context.RequestAborted, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default + ).Unwrap(); + + var writeTask = Task.Factory.StartNew(async () => + { + bool reqAborted = false; + + await foreach (var (chunkBuffer, length) in channel.Reader.ReadAllAsync(context.RequestAborted)) + { + try + { + if (reqAborted == false) + await response.Body.WriteAsync(chunkBuffer, 0, length, context.RequestAborted); + } + catch + { + reqAborted = true; + } + finally + { + ArrayPool.Shared.Return(chunkBuffer); + } + } + }, + context.RequestAborted, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default + ).Unwrap(); + + await Task.WhenAll(readTask, writeTask).ConfigureAwait(false); + #endregion + } + else + { + byte[] buffer = ArrayPool.Shared.Rent(PoolInvk.rentChunk); + + try + { + if (uriKeyFileCache != null && + responseMessage.Content.Headers.ContentLength.HasValue && + AppInit.conf.serverproxy.maxlength_ts >= responseMessage.Content.Headers.ContentLength) + { + #region cache + string md5key = CrypTo.md5(uriKeyFileCache); + string targetFile = $"cache/hls/{md5key}"; + var semaphore = new SemaphorManager(targetFile, context.RequestAborted); + + try + { + await semaphore.WaitAsync().ConfigureAwait(false); + + int cacheLength = 0; + + using (var fileStream = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + { + int bytesRead; + while ((bytesRead = await responseStream.ReadAsync(buffer, context.RequestAborted).ConfigureAwait(false)) != 0) + { + cacheLength += bytesRead; + await fileStream.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false); + await response.Body.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false); + } + } + + if (!responseMessage.Content.Headers.ContentLength.HasValue || responseMessage.Content.Headers.ContentLength.Value == cacheLength) + { + cacheFiles[md5key] = cacheLength; + } + else + { + File.Delete(targetFile); + } + } + catch + { + File.Delete(targetFile); + throw; + } + finally + { + semaphore.Release(); + } + #endregion + } + else + { + #region bypass + int bytesRead; + + while ((bytesRead = await responseStream.ReadAsync(buffer, context.RequestAborted).ConfigureAwait(false)) != 0) + await response.Body.WriteAsync(buffer, 0, bytesRead, context.RequestAborted).ConfigureAwait(false); + #endregion + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/ProxyCub.cs b/Lampac/Engine/Middlewares/ProxyCub.cs new file mode 100644 index 0000000..fc1ac8e --- /dev/null +++ b/Lampac/Engine/Middlewares/ProxyCub.cs @@ -0,0 +1,549 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Models; +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class ProxyCub + { + #region ProxyCub + static FileSystemWatcher fileWatcher; + + static readonly ConcurrentDictionary cacheFiles = new (); + + public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count; + + static Timer cleanupTimer; + + static ProxyCub() + { + Directory.CreateDirectory("cache/cub"); + + foreach (string path in Directory.EnumerateFiles("cache/cub", "*")) + { + using (var handle = File.OpenHandle(path)) + cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle)); + } + + fileWatcher = new FileSystemWatcher + { + Path = "cache/cub", + NotifyFilter = NotifyFilters.FileName, + EnableRaisingEvents = true + }; + + //fileWatcher.Created += (s, e) => { cacheFiles.TryAdd(e.Name, 0); }; + fileWatcher.Deleted += (s, e) => { cacheFiles.TryRemove(e.Name, out _); }; + + cleanupTimer = new Timer(cleanup, null, TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(20)); + } + + static void cleanup(object state) + { + try + { + foreach (string md5fileName in cacheFiles.Keys) + { + if (!File.Exists(Path.Combine("cache", "cub", md5fileName))) + cacheFiles.TryRemove(md5fileName, out _); + } + } + catch { } + } + + public ProxyCub(RequestDelegate next) { } + #endregion + + async public Task InvokeAsync(HttpContext httpContext, IMemoryCache memoryCache) + { + using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted)) + { + ctsHttp.CancelAfter(TimeSpan.FromSeconds(30)); + + var requestInfo = httpContext.Features.Get(); + var hybridCache = IHybridCache.Get(requestInfo); + + var init = AppInit.conf.cub; + string domain = init.domain; + string path = httpContext.Request.Path.Value.Replace("/cub/", "", StringComparison.OrdinalIgnoreCase); + string query = httpContext.Request.QueryString.Value; + string uri = Regex.Match(path, "^[^/]+/(.*)", RegexOptions.IgnoreCase).Groups[1].Value + query; + + if (!init.enable || domain == "ws") + { + httpContext.Response.Redirect($"https://{path}/{query}"); + return; + } + + if (path.Split(".")[0] is "geo" or "tmdb" or "tmapi" or "apitmdb" or "imagetmdb" or "cdn" or "ad" or "ws") + domain = $"{path.Split(".")[0]}.{domain}"; + + if (domain.StartsWith("geo", StringComparison.OrdinalIgnoreCase)) + { + string country = requestInfo.Country; + if (string.IsNullOrEmpty(country)) + { + var ipify = await Http.Get("https://api.ipify.org/?format=json"); + if (ipify != null || !string.IsNullOrEmpty(ipify.Value("ip"))) + country = GeoIP2.Country(ipify.Value("ip")); + } + + await httpContext.Response.WriteAsync(country ?? "", ctsHttp.Token); + return; + } + + #region checker + if (path.StartsWith("api/checker", StringComparison.OrdinalIgnoreCase) || uri.StartsWith("api/checker", StringComparison.OrdinalIgnoreCase)) + { + if (HttpMethods.IsPost(httpContext.Request.Method)) + { + if (httpContext.Request.ContentType != null && + httpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + using (var reader = new StreamReader(httpContext.Request.Body, leaveOpen: true, bufferSize: PoolInvk.bufferSize)) + { + string form = await reader.ReadToEndAsync(); + + var match = Regex.Match(form, @"(?:^|&)data=([^&]+)"); + if (match.Success) + { + string dataValue = Uri.UnescapeDataString(match.Groups[1].Value); + await httpContext.Response.WriteAsync(dataValue, ctsHttp.Token); + return; + } + } + } + } + + await httpContext.Response.WriteAsync("ok", ctsHttp.Token); + return; + } + #endregion + + #region blacklist + if (uri.StartsWith("api/plugins/blacklist", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.ContentType = "application/json; charset=utf-8"; + await httpContext.Response.WriteAsync("[]", ctsHttp.Token); + return; + } + #endregion + + #region ads/log/metric + if (uri.StartsWith("api/metric/", StringComparison.OrdinalIgnoreCase) || uri.StartsWith("api/ad/stat", StringComparison.OrdinalIgnoreCase)) + { + await httpContext.Response.WriteAsJsonAsync(new { secuses = true }); + return; + } + + if (uri.StartsWith("api/ad/vast", StringComparison.OrdinalIgnoreCase)) + { + await httpContext.Response.WriteAsJsonAsync(new + { + secuses = true, + ad = new string[] { }, + day_of_month = DateTime.Now.Day, + days_in_month = 31, + month = DateTime.Now.Month + }); + return; + } + #endregion + + var proxyManager = new ProxyManager("cub_api", init); + var proxy = proxyManager.Get(); + + bool isMedia = Regex.IsMatch(uri, "\\.(jpe?g|png|gif|webp|ico|svg|mp4|js|css)", RegexOptions.IgnoreCase); + + if (0 >= init.cache_api || !HttpMethods.IsGet(httpContext.Request.Method) || isMedia || + (path.Split(".")[0] is "imagetmdb" or "cdn" or "ad") || + httpContext.Request.Headers.ContainsKey("token") || httpContext.Request.Headers.ContainsKey("profile")) + { + #region bypass or media cache + string md5key = CrypTo.md5($"{domain}:{uri}"); + string outFile = Path.Combine("cache", "cub", md5key); + + if (cacheFiles.ContainsKey(md5key)) + { + httpContext.Response.Headers["X-Cache-Status"] = "HIT"; + httpContext.Response.ContentType = getContentType(uri); + + if (init.responseContentLength && cacheFiles.ContainsKey(md5key)) + httpContext.Response.ContentLength = cacheFiles[md5key]; + + await httpContext.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false); + return; + } + + var handler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.None, + AllowAutoRedirect = true + }; + + handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; + + if (proxy != null) + { + handler.UseProxy = true; + handler.Proxy = proxy; + } + else { handler.UseProxy = false; } + + var client = FrendlyHttp.MessageClient("proxyRedirect", handler); + var request = CreateProxyHttpRequest(httpContext, new Uri($"{init.scheme}://{domain}/{uri}"), requestInfo, init.viewru && path.Split(".")[0] == "tmdb"); + + using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ctsHttp.Token).ConfigureAwait(false)) + { + if (init.cache_img > 0 && isMedia && HttpMethods.IsGet(httpContext.Request.Method) && AppInit.conf.mikrotik == false && response.StatusCode == HttpStatusCode.OK) + { + #region cache + httpContext.Response.ContentType = getContentType(uri); + httpContext.Response.Headers["X-Cache-Status"] = "MISS"; + + if (init.responseContentLength && response.Content?.Headers?.ContentLength > 0) + { + if (!AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType)) + httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; + } + + var semaphore = new SemaphorManager(outFile, ctsHttp.Token); + + try + { + await semaphore.WaitAsync().ConfigureAwait(false); + + byte[] buffer = ArrayPool.Shared.Rent(PoolInvk.rentChunk); + + try + { + int cacheLength = 0; + + using (var cacheStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + { + using (var responseStream = await response.Content.ReadAsStreamAsync(ctsHttp.Token).ConfigureAwait(false)) + { + int bytesRead; + + while ((bytesRead = await responseStream.ReadAsync(buffer, ctsHttp.Token).ConfigureAwait(false)) > 0) + { + cacheLength += bytesRead; + await cacheStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await httpContext.Response.Body.WriteAsync(buffer, 0, bytesRead, ctsHttp.Token).ConfigureAwait(false); + } + } + } + + if (!response.Content.Headers.ContentLength.HasValue || response.Content.Headers.ContentLength.Value == cacheLength) + { + cacheFiles[md5key] = cacheLength; + } + else + { + File.Delete(outFile); + } + } + catch + { + File.Delete(outFile); + throw; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + semaphore.Release(); + } + #endregion + } + else + { + httpContext.Response.Headers["X-Cache-Status"] = "bypass"; + await CopyProxyHttpResponse(httpContext, response, ctsHttp.Token).ConfigureAwait(false); + } + } + #endregion + } + else + { + #region cache string + string memkey = $"cubproxy:key2:{domain}:{uri}"; + (byte[] content, int statusCode, string contentType) cache = default; + + var semaphore = new SemaphorManager(memkey, ctsHttp.Token); + + try + { + await semaphore.WaitAsync().ConfigureAwait(false); + + if (!hybridCache.TryGetValue(memkey, out cache, inmemory: false)) + { + var headers = HeadersModel.Init(); + + if (!string.IsNullOrEmpty(requestInfo.Country)) + headers.Add(new HeadersModel("cf-connecting-ip", requestInfo.IP)); + + if (path.Split(".")[0] == "tmdb") + { + if (init.viewru) + headers.Add(new HeadersModel("cookie", "viewru=1")); + + headers.Add(new HeadersModel("user-agent", httpContext.Request.Headers.UserAgent.ToString())); + } + else + { + foreach (var header in httpContext.Request.Headers) + { + if (header.Key.ToLower() is "cookie" or "user-agent") + headers.Add(new HeadersModel(header.Key, header.Value.ToString())); + } + } + + var result = await Http.BaseGet($"{init.scheme}://{domain}/{uri}", timeoutSeconds: 10, proxy: proxy, headers: headers, statusCodeOK: false, useDefaultHeaders: false).ConfigureAwait(false); + if (string.IsNullOrEmpty(result.content)) + { + proxyManager.Refresh(); + httpContext.Response.StatusCode = (int)result.response.StatusCode; + return; + } + + cache.content = Encoding.UTF8.GetBytes(result.content); + cache.statusCode = (int)result.response.StatusCode; + cache.contentType = result.response.Content?.Headers?.ContentType?.ToString() ?? getContentType(uri); + + if (domain.StartsWith("tmdb") || domain.StartsWith("tmapi") || domain.StartsWith("apitmdb")) + { + if (result.content == "{\"blocked\":true}") + { + var header = HeadersModel.Init(("localrequest", AppInit.rootPasswd)); + string json = await Http.Get($"http://{AppInit.conf.listen.localhost}:{AppInit.conf.listen.port}/tmdb/api/{uri}", timeoutSeconds: 5, headers: header).ConfigureAwait(false); + if (!string.IsNullOrEmpty(json)) + { + cache.statusCode = 200; + cache.contentType = "application/json; charset=utf-8"; + cache.content = Encoding.UTF8.GetBytes(json); + } + } + } + + httpContext.Response.Headers["X-Cache-Status"] = "MISS"; + + if (cache.statusCode == 200) + { + proxyManager.Success(); + hybridCache.Set(memkey, cache, DateTime.Now.AddMinutes(init.cache_api), inmemory: false); + } + } + else + { + httpContext.Response.Headers["X-Cache-Status"] = "HIT"; + } + } + finally + { + semaphore.Release(); + } + + if (init.responseContentLength && !AppInit.CompressionMimeTypes.Contains(cache.contentType)) + httpContext.Response.ContentLength = cache.content.Length; + + httpContext.Response.StatusCode = cache.statusCode; + httpContext.Response.ContentType = cache.contentType; + await httpContext.Response.Body.WriteAsync(cache.content, ctsHttp.Token).ConfigureAwait(false); + #endregion + } + } + } + + + #region getContentType + static string getContentType(string uri) + { + return Path.GetExtension(uri).ToLowerInvariant() switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".ico" => "image/x-icon", + ".svg" => "image/svg+xml", + ".mp4" => "video/mp4", + ".js" => "application/javascript", + ".css" => "text/css", + _ => "application/octet-stream" + }; + } + #endregion + + #region CreateProxyHttpRequest + HttpRequestMessage CreateProxyHttpRequest(HttpContext context, Uri uri, RequestModel requestInfo, bool viewru) + { + var request = context.Request; + + var requestMessage = new HttpRequestMessage(); + + var requestMethod = request.Method; + if (HttpMethods.IsPost(requestMethod)) + { + var streamContent = new StreamContent(request.Body); + requestMessage.Content = streamContent; + } + + if (viewru) + request.Headers["Cookie"] = "viewru=1"; + + if (!string.IsNullOrEmpty(requestInfo.Country)) + request.Headers["Cf-Connecting-Ip"] = requestInfo.IP; + + #region Headers + foreach (var header in request.Headers) + { + string key = header.Key; + + if (key.Equals("host", StringComparison.OrdinalIgnoreCase) || + key.Equals("origin", StringComparison.OrdinalIgnoreCase) || + key.Equals("referer", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) || + key.Equals("accept-encoding", StringComparison.OrdinalIgnoreCase)) + continue; + + if (viewru && key.Equals("cookie", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.StartsWith("x-", StringComparison.OrdinalIgnoreCase)) + continue; + + if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) + { + if (requestMessage.Content?.Headers != null) + requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + } + } + #endregion + + requestMessage.Headers.Host = uri.Authority; + requestMessage.RequestUri = uri; + requestMessage.Method = new HttpMethod(request.Method); + //requestMessage.Version = new Version(2, 0); + + return requestMessage; + } + #endregion + + #region CopyProxyHttpResponse + async Task CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage, CancellationToken cancellationToken) + { + var response = context.Response; + response.StatusCode = (int)responseMessage.StatusCode; + + #region responseContentLength + if (AppInit.conf.cub.responseContentLength && responseMessage.Content?.Headers?.ContentLength > 0) + { + IEnumerable contentType = null; + if (responseMessage.Content?.Headers != null) + responseMessage.Content.Headers.TryGetValues("Content-Type", out contentType); + + string type = contentType?.FirstOrDefault()?.ToLower(); + + if (string.IsNullOrEmpty(type) || !AppInit.CompressionMimeTypes.Contains(type)) + response.ContentLength = responseMessage.Content.Headers.ContentLength; + } + #endregion + + #region UpdateHeaders + void UpdateHeaders(HttpHeaders headers) + { + foreach (var header in headers) + { + string key = header.Key; + + if (key.Equals("server", StringComparison.OrdinalIgnoreCase) || + key.Equals("transfer-encoding", StringComparison.OrdinalIgnoreCase) || + key.Equals("etag", StringComparison.OrdinalIgnoreCase) || + key.Equals("connection", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-security-policy", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-disposition", StringComparison.OrdinalIgnoreCase) || + key.Equals("content-length", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.StartsWith("x-", StringComparison.OrdinalIgnoreCase) || + key.StartsWith("alt-", StringComparison.OrdinalIgnoreCase)) + continue; + + if (key.StartsWith("access-control", StringComparison.OrdinalIgnoreCase)) + continue; + + var values = header.Value; + + using (var e = values.GetEnumerator()) + { + if (!e.MoveNext()) + continue; + + var first = e.Current; + + response.Headers[key] = e.MoveNext() + ? string.Join("; ", values) + : first; + } + } + } + #endregion + + UpdateHeaders(responseMessage.Headers); + UpdateHeaders(responseMessage.Content.Headers); + + using (var responseStream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) + { + if (response.Body == null) + throw new ArgumentNullException("destination"); + + if (!responseStream.CanRead && !responseStream.CanWrite) + throw new ObjectDisposedException("ObjectDisposed_StreamClosed"); + + if (!response.Body.CanRead && !response.Body.CanWrite) + throw new ObjectDisposedException("ObjectDisposed_StreamClosed"); + + if (!responseStream.CanRead) + throw new NotSupportedException("NotSupported_UnreadableStream"); + + if (!response.Body.CanWrite) + throw new NotSupportedException("NotSupported_UnwritableStream"); + + byte[] buffer = ArrayPool.Shared.Rent(PoolInvk.rentChunk); + + try + { + int bytesRead; + + while ((bytesRead = await responseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) + await response.Body.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/ProxyImg.cs b/Lampac/Engine/Middlewares/ProxyImg.cs new file mode 100644 index 0000000..810e3c3 --- /dev/null +++ b/Lampac/Engine/Middlewares/ProxyImg.cs @@ -0,0 +1,630 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Shared; +using Shared.Engine; +using Shared.Models; +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class ProxyImg + { + #region ProxyImg + static readonly Regex rexPath = new Regex("/proxyimg([^/]+)?/", RegexOptions.Compiled | RegexOptions.IgnoreCase); + static readonly Regex rexRsize = new Regex("/proxyimg:([0-9]+):([0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + static readonly FileSystemWatcher fileWatcher; + + static readonly ConcurrentDictionary cacheFiles = new(); + + public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count; + + static readonly Timer cleanupTimer; + + static ProxyImg() + { + Directory.CreateDirectory("cache/img"); + + if (AppInit.conf.multiaccess == false) + return; + + foreach (string path in Directory.EnumerateFiles("cache/img", "*")) + { + using (var handle = File.OpenHandle(path)) + cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle)); + } + + fileWatcher = new FileSystemWatcher + { + Path = "cache/img", + NotifyFilter = NotifyFilters.FileName, + EnableRaisingEvents = true + }; + + //fileWatcher.Created += (s, e) => + //{ + // cacheFiles.TryAdd(e.Name, 0); + //}; + + fileWatcher.Deleted += (s, e) => { cacheFiles.TryRemove(e.Name, out _); }; + + cleanupTimer = new Timer(cleanup, null, TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(20)); + } + + static void cleanup(object state) + { + try + { + foreach (string md5fileName in cacheFiles.Keys) + { + if (!File.Exists(Path.Combine("cache", "img", md5fileName))) + cacheFiles.TryRemove(md5fileName, out _); + } + } + catch { } + } + + public ProxyImg(RequestDelegate next) { } + #endregion + + async public Task InvokeAsync(HttpContext httpContext, IMemoryCache memoryCache) + { + using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContext.RequestAborted)) + { + ctsHttp.CancelAfter(TimeSpan.FromSeconds(30)); + + var requestInfo = httpContext.Features.Get(); + + var init = AppInit.conf.serverproxy.image; + bool cacheimg = init.cache && AppInit.conf.mikrotik == false; + + string servPath = rexPath.Replace(httpContext.Request.Path.Value, ""); + string href = servPath + httpContext.Request.QueryString.Value; + + #region Проверки + if (servPath.Contains("image.tmdb.org", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Response.Redirect($"/tmdb/img/{Regex.Replace(href.Replace("://", ":/_/").Replace("//", "/").Replace(":/_/", "://"), "^https?://[^/]+/", "")}"); + return; + } + + var decryptLink = ProxyLink.Decrypt(servPath, requestInfo.IP); + + if (AppInit.conf.serverproxy.encrypt || decryptLink?.uri != null) + { + href = decryptLink?.uri; + } + else + { + if (!AppInit.conf.serverproxy.enable) + { + httpContext.Response.StatusCode = 403; + return; + } + } + + if (string.IsNullOrWhiteSpace(href) || !href.StartsWith("http")) + { + httpContext.Response.StatusCode = 404; + return; + } + #endregion + + if (AppInit.conf.serverproxy.showOrigUri) + httpContext.Response.Headers["PX-Orig"] = href; + + #region width / height + int width = 0; + int height = 0; + + if (httpContext.Request.Path.Value.StartsWith("/proxyimg:", StringComparison.OrdinalIgnoreCase)) + { + if (!cacheimg) + cacheimg = init.cache_rsize; + + var gimg = rexRsize.Match(httpContext.Request.Path.Value).Groups; + width = int.Parse(gimg[1].Value); + height = int.Parse(gimg[2].Value); + } + #endregion + + string md5key = CrypTo.md5($"{href}:{width}:{height}"); + + if (InvkEvent.IsProxyImgMd5key()) + InvkEvent.ProxyImgMd5key(ref md5key, httpContext, requestInfo, decryptLink, href, width, height); + + string outFile = Path.Combine("cache", "img", md5key); + + string url_reserve = null; + if (href.Contains(" or ")) + { + var urls = href.Split(" or "); + href = urls[0]; + url_reserve = urls[1]; + } + + string contentType = href.Contains(".png", StringComparison.OrdinalIgnoreCase) + ? "image/png" + : href.Contains(".webp", StringComparison.OrdinalIgnoreCase) ? "image/webp" : "image/jpeg"; + + if (width > 0 || height > 0) + contentType = href.Contains(".png", StringComparison.OrdinalIgnoreCase) ? "image/png" : "image/jpeg"; + + #region cacheFiles + if (cacheimg) + { + if (cacheFiles.ContainsKey(md5key) || (AppInit.conf.multiaccess == false && File.Exists(outFile))) + { + httpContext.Response.Headers["X-Cache-Status"] = "HIT"; + httpContext.Response.ContentType = contentType; + + if (AppInit.conf.serverproxy.responseContentLength && cacheFiles.ContainsKey(md5key)) + httpContext.Response.ContentLength = cacheFiles[md5key]; + + await httpContext.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false); + return; + } + } + #endregion + + var semaphore = cacheimg ? new SemaphorManager(outFile, ctsHttp.Token) : null; + + try + { + string memKeyErrorDownload = $"ProxyImg:ErrorDownload:{href}"; + if (memoryCache.TryGetValue(memKeyErrorDownload, out _)) + { + httpContext.Response.Redirect(href); + return; + } + + if (semaphore != null) + await semaphore.WaitAsync().ConfigureAwait(false); + + #region cacheFiles + if (cacheimg) + { + if (cacheFiles.ContainsKey(md5key) || (AppInit.conf.multiaccess == false && File.Exists(outFile))) + { + httpContext.Response.Headers["X-Cache-Status"] = "HIT"; + httpContext.Response.ContentType = contentType; + + if (AppInit.conf.serverproxy.responseContentLength && cacheFiles.ContainsKey(md5key)) + httpContext.Response.ContentLength = cacheFiles[md5key]; + + semaphore?.Release(); + await httpContext.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false); + return; + } + } + #endregion + + httpContext.Response.Headers["X-Cache-Status"] = cacheimg ? "MISS" : "bypass"; + + #region proxyManager + ProxyManager proxyManager = null; + + if (decryptLink?.plugin == "posterapi" && AppInit.conf.posterApi.useproxy) + proxyManager = new ProxyManager("posterapi", AppInit.conf.posterApi); + + if (proxyManager == null && init.useproxy) + proxyManager = new ProxyManager("proxyimg", init); + + WebProxy proxy = proxyManager?.Get(); + #endregion + + if (width == 0 && height == 0) + { + #region bypass + bypass_reset: + var handler = Http.Handler(href, proxy); + + var client = FrendlyHttp.MessageClient("proxyimg", handler); + + var req = new HttpRequestMessage(HttpMethod.Get, href) + { + Version = HttpVersion.Version11 + }; + + bool useDefaultHeaders = true; + if (decryptLink?.headers != null && decryptLink.headers.Count > 0 && decryptLink.headers.FirstOrDefault(i => i.name.ToLower() == "user-agent") != null) + useDefaultHeaders = false; + + Http.DefaultRequestHeaders(href, req, null, null, decryptLink?.headers, useDefaultHeaders: useDefaultHeaders); + + using (HttpResponseMessage response = await client.SendAsync(req, ctsHttp.Token).ConfigureAwait(false)) + { + if (response.StatusCode != HttpStatusCode.OK) + { + if (url_reserve != null) + { + href = url_reserve; + url_reserve = null; + goto bypass_reset; + } + + if (cacheimg) + memoryCache.Set(memKeyErrorDownload, 0, DateTime.Now.AddSeconds(5)); + + proxyManager?.Refresh(); + httpContext.Response.Redirect(href); + return; + } + + httpContext.Response.StatusCode = (int)response.StatusCode; + + if (response.Content.Headers.TryGetValues("Content-Type", out var contype)) + httpContext.Response.ContentType = contype?.FirstOrDefault()?.ToLowerAndTrim() ?? contentType; + else + httpContext.Response.ContentType = contentType; + + if (AppInit.conf.serverproxy.responseContentLength && response.Content?.Headers?.ContentLength > 0) + { + if (!AppInit.CompressionMimeTypes.Contains(httpContext.Response.ContentType)) + httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; + } + + if (cacheimg) + { + byte[] buffer = ArrayPool.Shared.Rent(PoolInvk.rentChunk); + + try + { + int cacheLength = 0; + + using (var cacheStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + { + using (var responseStream = await response.Content.ReadAsStreamAsync(ctsHttp.Token).ConfigureAwait(false)) + { + int bytesRead; + + while ((bytesRead = await responseStream.ReadAsync(buffer, ctsHttp.Token).ConfigureAwait(false)) > 0) + { + cacheLength += bytesRead; + await cacheStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await httpContext.Response.Body.WriteAsync(buffer, 0, bytesRead, ctsHttp.Token).ConfigureAwait(false); + } + } + } + + if (!response.Content.Headers.ContentLength.HasValue || response.Content.Headers.ContentLength.Value == cacheLength) + { + if (AppInit.conf.multiaccess) + cacheFiles[md5key] = cacheLength; + } + else + { + File.Delete(outFile); + } + } + catch + { + File.Delete(outFile); + throw; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + else + { + await response.Content.CopyToAsync(httpContext.Response.Body, ctsHttp.Token).ConfigureAwait(false); + } + } + #endregion + } + else + { + #region rsize + httpContext.Response.ContentType = contentType; + + rsize_reset: + + using (var inArray = PoolInvk.msm.GetStream()) + { + var result = await Download(inArray, href, ctsHttp.Token, proxy: proxy, headers: decryptLink?.headers).ConfigureAwait(false); + + if (!result.success) + { + if (url_reserve != null) + { + href = url_reserve; + url_reserve = null; + goto rsize_reset; + } + + if (cacheimg) + memoryCache.Set(memKeyErrorDownload, 0, DateTime.Now.AddSeconds(5)); + + proxyManager?.Refresh(); + httpContext.Response.Redirect(href); + return; + } + + using (var outArray = PoolInvk.msm.GetStream()) + { + bool successConvert = false; + + if ((result.contentType ?? contentType) is "image/png" or "image/webp" or "image/jpeg") + { + if (AppInit.conf.imagelibrary == "NetVips") + { + successConvert = NetVipsImage(href, inArray, outArray, width, height); + } + else if (AppInit.conf.imagelibrary == "ImageMagick" && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + successConvert = await ImageMagick(inArray, outArray, width, height, cacheimg ? outFile : null); + + if (cacheimg) + { + if (successConvert) + { + inArray.Dispose(); + outArray.Dispose(); + semaphore?.Release(); + + if (AppInit.conf.multiaccess) + { + using (var handle = File.OpenHandle(outFile)) + cacheFiles[md5key] = (int)RandomAccess.GetLength(handle); + } + + await httpContext.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false); + return; + } + + proxyManager?.Refresh(); + httpContext.Response.Redirect(href); + return; + } + } + } + + var resultArray = successConvert ? outArray : inArray; + resultArray.Position = 0; + + if (successConvert) + proxyManager?.Success(); + + if (AppInit.conf.serverproxy.responseContentLength) + httpContext.Response.ContentLength = resultArray.Length; + + try + { + await resultArray.CopyToAsync(httpContext.Response.Body, PoolInvk.bufferSize, ctsHttp.Token).ConfigureAwait(false); + + if (cacheimg) + await TrySaveCache(resultArray, outFile, md5key); + } + catch + { + if (cacheimg) + await TrySaveCache(resultArray, outFile, md5key); + + throw; + } + } + } + #endregion + } + } + finally + { + if (semaphore != null) + semaphore.Release(); + } + } + } + + + #region Download + async Task<(bool success, string contentType)> Download(Stream ms, string url, CancellationToken cancellationToken, List headers = null, WebProxy proxy = null) + { + try + { + var handler = Http.Handler(url, proxy); + + var client = FrendlyHttp.MessageClient("base", handler); + + var req = new HttpRequestMessage(HttpMethod.Get, url) + { + Version = HttpVersion.Version11 + }; + + + bool useDefaultHeaders = true; + if (headers != null && headers.Count > 0 && headers.FirstOrDefault(i => i.name.ToLowerInvariant() == "user-agent") != null) + useDefaultHeaders = false; + + Http.DefaultRequestHeaders(url, req, null, null, headers, useDefaultHeaders: useDefaultHeaders); + + using (HttpResponseMessage response = await client.SendAsync(req, cancellationToken).ConfigureAwait(false)) + { + if (response.StatusCode != HttpStatusCode.OK) + return default; + + using (HttpContent content = response.Content) + { + await content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); + if (ms.Length == 0 || 1000 > ms.Length) + return default; + + if (content.Headers != null) + { + if (content.Headers.ContentLength.HasValue && content.Headers.ContentLength != ms.Length) + return default; + + response.Content.Headers.TryGetValues("Content-Type", out var _contentType); + + ms.Position = 0; + return (true, _contentType?.FirstOrDefault()?.ToLower()); + } + + ms.Position = 0; + return (true, null); + } + } + } + catch + { + return default; + } + } + #endregion + + #region TrySaveCache + async Task TrySaveCache(Stream ms, string outFile, string md5key) + { + try + { + ms.Position = 0; + + using (var streamFile = File.OpenWrite(outFile)) + await ms.CopyToAsync(streamFile, PoolInvk.bufferSize); + + if (AppInit.conf.multiaccess) + cacheFiles[md5key] = (int)ms.Length; + } + catch { File.Delete(outFile); } + } + #endregion + + #region NetVipsImage + static bool _initNetVips = false; + + private bool NetVipsImage(string href, Stream inArray, Stream outArray, int width, int height) + { + if (!_initNetVips) + { + _initNetVips = true; + NetVips.Cache.Max = 0; // 0 операций в кэше + NetVips.Cache.MaxMem = 0; // 0 байт памяти под кэш + NetVips.Cache.MaxFiles = 0; // 0 файлов в файловом кэше + NetVips.Cache.Trace = false; + } + + try + { + using (var image = NetVips.Image.NewFromStream(inArray, access: NetVips.Enums.Access.Sequential)) + { + if ((width != 0 && image.Width > width) || (height != 0 && image.Height > height)) + { + using (var res = image.ThumbnailImage(width == 0 ? image.Width : width, height == 0 ? image.Height : height, crop: NetVips.Enums.Interesting.None)) + { + if (href.Contains(".png", StringComparison.OrdinalIgnoreCase)) + res.PngsaveStream(outArray); + else + res.JpegsaveStream(outArray); + + if (outArray.Length > 1000) + return true; + } + } + } + } + catch { } + + return false; + } + #endregion + + #region ImageMagick + static string imaGikPath = null; + + /// + /// apt install -y imagemagick libpng-dev libjpeg-dev libwebp-dev + /// + async static Task ImageMagick(Stream inArray, Stream outArray, int width, int height, string outputFilePath) + { + if (imaGikPath == null) + imaGikPath = File.Exists("/usr/bin/magick") ? "magick" : "convert"; + + string inputFilePath = getTempFileName(); + + bool outFileIsTemp = false; + if (outputFilePath == null) + { + outFileIsTemp = true; + outputFilePath = getTempFileName(); + } + + try + { + inArray.Position = 0; + + using (var streamFile = File.OpenWrite(inputFilePath)) + await inArray.CopyToAsync(streamFile, PoolInvk.bufferSize); + + string argsize = width > 0 && height > 0 ? $"{width}x{height}" : width > 0 ? $"{width}x" : $"x{height}"; + + using (Process process = new Process()) + { + process.StartInfo.FileName = imaGikPath; + process.StartInfo.Arguments = $"\"{inputFilePath}\" -resize {argsize} \"{outputFilePath}\""; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + process.Start(); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + return false; + } + + if (outFileIsTemp) + { + using (var streamFile = File.OpenRead(outputFilePath)) + await streamFile.CopyToAsync(outArray, PoolInvk.bufferSize); + } + + return true; + } + catch + { + return false; + } + finally + { + try + { + if (File.Exists(inputFilePath)) + File.Delete(inputFilePath); + + if (outFileIsTemp && File.Exists(outputFilePath)) + File.Delete(outputFilePath); + } + catch { } + } + } + + + static bool? shm = null; + + static string getTempFileName() + { + if (shm == null) + shm = Directory.Exists("/dev/shm"); + + if (shm == true) + return $"/dev/shm/{CrypTo.md5(DateTime.Now.ToFileTimeUtc().ToString())}"; + + return Path.GetTempFileName(); + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/ProxyTmdb.cs b/Lampac/Engine/Middlewares/ProxyTmdb.cs new file mode 100644 index 0000000..535ee5a --- /dev/null +++ b/Lampac/Engine/Middlewares/ProxyTmdb.cs @@ -0,0 +1,462 @@ +using DnsClient; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json.Linq; +using Shared; +using Shared.Engine; +using Shared.Engine.Utilities; +using Shared.Models; +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class ProxyTmdb + { + #region ProxyTmdb + static FileSystemWatcher fileWatcher; + + static readonly ConcurrentDictionary cacheFiles = new (); + + public static int Stat_ContCacheFiles => cacheFiles.IsEmpty ? 0 : cacheFiles.Count; + + static Timer cleanupTimer; + + static ProxyTmdb() + { + Directory.CreateDirectory("cache/tmdb"); + + if (AppInit.conf.multiaccess == false) + return; + + foreach (string path in Directory.EnumerateFiles("cache/tmdb", "*")) + { + using (var handle = File.OpenHandle(path)) + cacheFiles.TryAdd(Path.GetFileName(path), (int)RandomAccess.GetLength(handle)); + } + + fileWatcher = new FileSystemWatcher + { + Path = "cache/tmdb", + NotifyFilter = NotifyFilters.FileName, + EnableRaisingEvents = true + }; + + //fileWatcher.Created += (s, e) => { cacheFiles.TryAdd(e.Name, 0); }; + fileWatcher.Deleted += (s, e) => { cacheFiles.TryRemove(e.Name, out _); }; + + cleanupTimer = new Timer(cleanup, null, TimeSpan.FromMinutes(20), TimeSpan.FromMinutes(20)); + } + + static void cleanup(object state) + { + try + { + foreach (string md5fileName in cacheFiles.Keys) + { + if (!File.Exists(Path.Combine("cache", "tmdb", md5fileName))) + cacheFiles.TryRemove(md5fileName, out _); + } + } + catch { } + } + + public ProxyTmdb(RequestDelegate next) { } + #endregion + + public Task Invoke(HttpContext httpContext) + { + var requestInfo = httpContext.Features.Get(); + var hybridCache = IHybridCache.Get(requestInfo); + + if (httpContext.Request.Path.Value.StartsWith("/tmdb/api/", StringComparison.OrdinalIgnoreCase)) + return API(httpContext, hybridCache, requestInfo); + + if (httpContext.Request.Path.Value.StartsWith("/tmdb/img/", StringComparison.OrdinalIgnoreCase)) + return IMG(httpContext, requestInfo); + + string path = Regex.Replace(httpContext.Request.Path.Value, "^/tmdb/https?://", "", RegexOptions.IgnoreCase).Replace("/tmdb/", ""); + string uri = Regex.Match(path, "^[^/]+/(.*)", RegexOptions.IgnoreCase).Groups[1].Value + httpContext.Request.QueryString.Value; + + if (path.Contains("api.themoviedb.org", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Request.Path = $"/tmdb/api/{uri}"; + return API(httpContext, hybridCache, requestInfo); + } + else if (path.Contains("image.tmdb.org", StringComparison.OrdinalIgnoreCase)) + { + httpContext.Request.Path = $"/tmdb/img/{uri}"; + return IMG(httpContext, requestInfo); + } + + httpContext.Response.StatusCode = 403; + return Task.CompletedTask; + } + + + #region API + async public Task API(HttpContext httpContex, IHybridCache hybridCache, RequestModel requestInfo) + { + using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContex.RequestAborted)) + { + ctsHttp.CancelAfter(TimeSpan.FromSeconds(30)); + httpContex.Response.ContentType = "application/json; charset=utf-8"; + + var init = AppInit.conf.tmdb; + if (!init.enable && !requestInfo.IsLocalRequest) + { + httpContex.Response.StatusCode = 401; + await httpContex.Response.WriteAsJsonAsync(new { error = true, msg = "disable" }, ctsHttp.Token); + return; + } + + string path = httpContex.Request.Path.Value.Replace("/tmdb/api", "", StringComparison.OrdinalIgnoreCase); + path = Regex.Replace(path, "^/https?://api.themoviedb.org", "", RegexOptions.IgnoreCase); + path = Regex.Replace(path, "/$", "", RegexOptions.IgnoreCase); + + string query = Regex.Replace(httpContex.Request.QueryString.Value, "(&|\\?)(account_email|email|uid|token)=[^&]+", ""); + string uri = "https://api.themoviedb.org" + path + query; + + string mkey = $"tmdb/api:{path}:{query}"; + + if (hybridCache.TryGetValue(mkey, out (string json, int statusCode) cache, inmemory: false)) + { + httpContex.Response.Headers["X-Cache-Status"] = "HIT"; + httpContex.Response.StatusCode = cache.statusCode; + httpContex.Response.ContentType = "application/json; charset=utf-8"; + await httpContex.Response.WriteAsync(cache.json, ctsHttp.Token); + return; + } + + httpContex.Response.Headers["X-Cache-Status"] = "MISS"; + + string tmdb_ip = init.API_IP; + + #region DNS QueryType.A + if (string.IsNullOrEmpty(tmdb_ip) && string.IsNullOrEmpty(init.API_Minor) && !string.IsNullOrEmpty(init.DNS)) + { + string dnskey = $"tmdb/api:dns:{init.DNS}"; + + var _spredns = new SemaphorManager(dnskey, ctsHttp.Token); + + try + { + await _spredns.WaitAsync(); + + if (!Startup.memoryCache.TryGetValue(dnskey, out string dns_ip)) + { + var lookup = new LookupClient(IPAddress.Parse(init.DNS)); + var queryType = await lookup.QueryAsync("api.themoviedb.org", QueryType.A, cancellationToken: ctsHttp.Token); + dns_ip = queryType?.Answers?.ARecords()?.FirstOrDefault()?.Address?.ToString(); + + if (!string.IsNullOrEmpty(dns_ip)) + Startup.memoryCache.Set(dnskey, dns_ip, DateTime.Now.AddMinutes(Math.Max(init.DNS_TTL, 5))); + else + Startup.memoryCache.Set(dnskey, string.Empty, DateTime.Now.AddMinutes(5)); + } + + if (!string.IsNullOrEmpty(dns_ip)) + tmdb_ip = dns_ip; + } + catch { } + finally + { + _spredns.Release(); + } + } + #endregion + + var headers = new List(); + + var proxyManager = init.useproxy + ? new ProxyManager("tmdb_api", init) + : null; + + if (!string.IsNullOrEmpty(init.API_Minor)) + { + uri = uri.Replace("api.themoviedb.org", init.API_Minor); + } + else if (!string.IsNullOrEmpty(tmdb_ip)) + { + headers.Add(new HeadersModel("Host", "api.themoviedb.org")); + uri = uri.Replace("api.themoviedb.org", tmdb_ip); + } + + var result = await Http.BaseGetAsync(uri, timeoutSeconds: 20, proxy: proxyManager?.Get(), httpversion: init.httpversion, headers: headers, statusCodeOK: false); + if (result.content == null) + { + proxyManager?.Refresh(); + httpContex.Response.StatusCode = 401; + await httpContex.Response.WriteAsJsonAsync(new { error = true, msg = "json null" }, ctsHttp.Token); + return; + } + + cache.statusCode = (int)result.response.StatusCode; + httpContex.Response.StatusCode = cache.statusCode; + + if (result.content.ContainsKey("status_message") || result.response.StatusCode != HttpStatusCode.OK) + { + proxyManager?.Refresh(); + cache.json = JsonConvertPool.SerializeObject(result.content); + + if (init.cache_api > 0 && !string.IsNullOrEmpty(cache.json)) + hybridCache.Set(mkey, cache, DateTime.Now.AddMinutes(1), inmemory: true); + + await httpContex.Response.WriteAsync(cache.json, ctsHttp.Token); + return; + } + + cache.json = JsonConvertPool.SerializeObject(result.content); + + if (init.cache_api > 0 && !string.IsNullOrEmpty(cache.json)) + hybridCache.Set(mkey, cache, DateTime.Now.AddMinutes(init.cache_api), inmemory: false); + + proxyManager?.Success(); + httpContex.Response.ContentType = "application/json; charset=utf-8"; + await httpContex.Response.WriteAsync(cache.json, ctsHttp.Token); + } + } + #endregion + + #region IMG + async public Task IMG(HttpContext httpContex, RequestModel requestInfo) + { + using (var ctsHttp = CancellationTokenSource.CreateLinkedTokenSource(httpContex.RequestAborted)) + { + ctsHttp.CancelAfter(TimeSpan.FromSeconds(30)); + + var init = AppInit.conf.tmdb; + if (!init.enable) + { + httpContex.Response.StatusCode = 401; + await httpContex.Response.WriteAsJsonAsync(new { error = true, msg = "disable" }, ctsHttp.Token); + return; + } + + string path = httpContex.Request.Path.Value.Replace("/tmdb/img", "", StringComparison.OrdinalIgnoreCase); + path = Regex.Replace(path, "^/https?://image.tmdb.org", "", RegexOptions.IgnoreCase); + + string query = Regex.Replace(httpContex.Request.QueryString.Value, "(&|\\?)(account_email|email|uid|token)=[^&]+", ""); + string uri = "https://image.tmdb.org" + path + query; + + string md5key = CrypTo.md5($"{path}:{query}"); + string outFile = Path.Combine("cache", "tmdb", md5key); + + bool cacheimg = init.cache_img > 0 && AppInit.conf.mikrotik == false; + + httpContex.Response.ContentType = path.Contains(".png", StringComparison.OrdinalIgnoreCase) + ? "image/png" + : path.Contains(".svg", StringComparison.OrdinalIgnoreCase) ? "image/svg+xml" : "image/jpeg"; + + #region cacheFiles + if (cacheimg) + { + if (cacheFiles.ContainsKey(md5key) || (AppInit.conf.multiaccess == false && File.Exists(outFile))) + { + httpContex.Response.Headers["X-Cache-Status"] = "HIT"; + + if (init.responseContentLength && cacheFiles.ContainsKey(md5key)) + httpContex.Response.ContentLength = cacheFiles[md5key]; + + await httpContex.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false); + return; + } + } + #endregion + + string tmdb_ip = init.IMG_IP; + + #region DNS QueryType.A + if (string.IsNullOrEmpty(tmdb_ip) && string.IsNullOrEmpty(init.IMG_Minor) && !string.IsNullOrEmpty(init.DNS)) + { + string dnskey = $"tmdb/img:dns:{init.DNS}"; + + var _spredns = new SemaphorManager(dnskey, ctsHttp.Token); + + try + { + await _spredns.WaitAsync(); + + if (!Startup.memoryCache.TryGetValue(dnskey, out string dns_ip)) + { + var lookup = new LookupClient(IPAddress.Parse(init.DNS)); + var result = await lookup.QueryAsync("image.tmdb.org", QueryType.A, cancellationToken: ctsHttp.Token); + dns_ip = result?.Answers?.ARecords()?.FirstOrDefault()?.Address?.ToString(); + + if (!string.IsNullOrEmpty(dns_ip)) + Startup.memoryCache.Set(dnskey, dns_ip, DateTime.Now.AddMinutes(Math.Max(init.DNS_TTL, 5))); + else + Startup.memoryCache.Set(dnskey, string.Empty, DateTime.Now.AddMinutes(5)); + } + + if (!string.IsNullOrEmpty(dns_ip)) + tmdb_ip = dns_ip; + } + catch { } + finally + { + _spredns.Release(); + } + } + #endregion + + #region headers + var headers = HeadersModel.Init( + // используем старый ua что-бы гарантировать image/jpeg вместо image/webp + ("Accept", "image/jpeg,image/png,image/*;q=0.8,*/*;q=0.5"), + ("User-Agent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2"), + ("Cache-Control", "max-age=0") + ); + + if (!string.IsNullOrEmpty(init.IMG_Minor)) + { + uri = uri.Replace("image.tmdb.org", init.IMG_Minor); + } + else if (!string.IsNullOrEmpty(tmdb_ip)) + { + headers.Add(new HeadersModel("Host", "image.tmdb.org")); + uri = uri.Replace("image.tmdb.org", tmdb_ip); + } + #endregion + + var proxyManager = init.useproxy + ? new ProxyManager("tmdb_img", init) + : null; + + var semaphore = cacheimg ? new SemaphorManager(outFile, ctsHttp.Token) : null; + + try + { + if (semaphore != null) + await semaphore.WaitAsync().ConfigureAwait(false); + + #region cacheFiles + if (cacheimg) + { + if (cacheFiles.ContainsKey(md5key) || (AppInit.conf.multiaccess == false && File.Exists(outFile))) + { + httpContex.Response.Headers["X-Cache-Status"] = "HIT"; + + if (init.responseContentLength && cacheFiles.ContainsKey(md5key)) + httpContex.Response.ContentLength = cacheFiles[md5key]; + + semaphore?.Release(); + await httpContex.Response.SendFileAsync(outFile, ctsHttp.Token).ConfigureAwait(false); + return; + } + } + #endregion + + var handler = Http.Handler(uri, proxyManager?.Get()); + + var client = FrendlyHttp.MessageClient(init.httpversion == 2 ? "http2proxyimg" : "proxyimg", handler); + + var req = new HttpRequestMessage(HttpMethod.Get, uri) + { + Version = init.httpversion == 1 ? HttpVersion.Version11 : new Version(init.httpversion, 0) + }; + + foreach (var h in headers) + { + if (!req.Headers.TryAddWithoutValidation(h.name, h.val)) + { + if (req.Content?.Headers != null) + req.Content.Headers.TryAddWithoutValidation(h.name, h.val); + } + } + + using (HttpResponseMessage response = await client.SendAsync(req, ctsHttp.Token).ConfigureAwait(false)) + { + if (response.StatusCode == HttpStatusCode.OK) + proxyManager?.Success(); + else + proxyManager?.Refresh(); + + httpContex.Response.StatusCode = (int)response.StatusCode; + + if (init.responseContentLength && response.Content?.Headers?.ContentLength > 0) + httpContex.Response.ContentLength = response.Content.Headers.ContentLength.Value; + + if (response.StatusCode == HttpStatusCode.OK && cacheimg) + { + #region cache + httpContex.Response.Headers["X-Cache-Status"] = "MISS"; + + byte[] buffer = ArrayPool.Shared.Rent(PoolInvk.rentChunk); + + try + { + int cacheLength = 0; + + using (var cacheStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None, PoolInvk.bufferSize)) + { + using (var responseStream = await response.Content.ReadAsStreamAsync(ctsHttp.Token).ConfigureAwait(false)) + { + int bytesRead; + + while ((bytesRead = await responseStream.ReadAsync(buffer, ctsHttp.Token).ConfigureAwait(false)) > 0) + { + cacheLength += bytesRead; + await cacheStream.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + await httpContex.Response.Body.WriteAsync(buffer, 0, bytesRead, ctsHttp.Token).ConfigureAwait(false); + } + } + } + + if (!response.Content.Headers.ContentLength.HasValue || response.Content.Headers.ContentLength.Value == cacheLength) + { + if (AppInit.conf.multiaccess) + cacheFiles[md5key] = cacheLength; + } + else + { + File.Delete(outFile); + } + } + catch + { + File.Delete(outFile); + throw; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + #endregion + } + else + { + semaphore?.Release(); + httpContex.Response.Headers["X-Cache-Status"] = "bypass"; + await response.Content.CopyToAsync(httpContex.Response.Body, ctsHttp.Token).ConfigureAwait(false); + } + } + } + catch + { + proxyManager?.Refresh(); + + if (!string.IsNullOrEmpty(tmdb_ip)) + httpContex.Response.Redirect(uri.Replace(tmdb_ip, "image.tmdb.org")); + else + httpContex.Response.Redirect(uri); + } + finally + { + if (semaphore != null) + semaphore.Release(); + } + } + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/RequestInfo.cs b/Lampac/Engine/Middlewares/RequestInfo.cs new file mode 100644 index 0000000..e7a9699 --- /dev/null +++ b/Lampac/Engine/Middlewares/RequestInfo.cs @@ -0,0 +1,274 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Primitives; +using Shared; +using Shared.Models; +using System; +using System.IO; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class RequestInfo + { + #region RequestInfo + private readonly RequestDelegate _next; + IMemoryCache memoryCache; + + public RequestInfo(RequestDelegate next, IMemoryCache mem) + { + _next = next; + memoryCache = mem; + } + #endregion + + public Task Invoke(HttpContext httpContext) + { + bool IsWsRequest = httpContext.Request.Path.StartsWithSegments("/nws", StringComparison.OrdinalIgnoreCase) || + httpContext.Request.Path.StartsWithSegments("/ws", StringComparison.OrdinalIgnoreCase); + + #region stats + if (AppInit.conf.openstat.enable && !IsWsRequest) + { + var now = DateTime.UtcNow; + var counter = memoryCache.GetOrCreate($"stats:request:{now.Hour}:{now.Minute}", entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60); + return new CounterRequestInfo(); + }); + + Interlocked.Increment(ref counter.Value); + } + #endregion + + bool IsLocalRequest = false; + string cf_country = null; + string clientIp = httpContext.Connection.RemoteIpAddress.ToString(); + bool IsLocalIp = Shared.Engine.Utilities.IPNetwork.IsLocalIp(clientIp); + + if (httpContext.Request.Headers.TryGetValue("localrequest", out StringValues _localpasswd) && _localpasswd.Count > 0) + { + if (!IsLocalIp && !AppInit.conf.BaseModule.allowExternalIpAccessToLocalRequest) + return httpContext.Response.WriteAsync("allowExternalIpAccessToLocalRequest false", httpContext.RequestAborted); + + if (_localpasswd[0] != AppInit.rootPasswd) + return httpContext.Response.WriteAsync("error passwd", httpContext.RequestAborted); + + IsLocalRequest = true; + + if (httpContext.Request.Headers.TryGetValue("x-client-ip", out StringValues xip) && xip.Count > 0) + { + if (!string.IsNullOrEmpty(xip[0])) + clientIp = xip[0]; + } + } + else if (AppInit.conf.real_ip_cf || AppInit.conf.listen.frontend == "cloudflare") + { + #region cloudflare + if (Program.cloudflare_ips != null && Program.cloudflare_ips.Count > 0) + { + try + { + var clientIPAddress = IPAddress.Parse(clientIp); + foreach (var cf in Program.cloudflare_ips) + { + if (new System.Net.IPNetwork(cf.prefix, cf.prefixLength).Contains(clientIPAddress)) + { + if (httpContext.Request.Headers.TryGetValue("CF-Connecting-IP", out StringValues xip) && xip.Count > 0) + { + if (!string.IsNullOrEmpty(xip[0])) + clientIp = xip[0]; + } + + if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues xfp) && xfp.Count > 0) + { + if (!string.IsNullOrEmpty(xfp[0])) + { + if (xfp[0] == "http" || xfp[0] == "https") + httpContext.Request.Scheme = xfp; + } + } + + if (httpContext.Request.Headers.TryGetValue("CF-IPCountry", out StringValues xcountry) && xcountry.Count > 0) + { + if (!string.IsNullOrEmpty(xcountry[0])) + cf_country = xcountry[0]; + } + + break; + } + } + } + catch { } + } + #endregion + } + // запрос с cloudflare, запрос не в админку + else if (httpContext.Request.Headers.ContainsKey("CF-Connecting-IP") && !httpContext.Request.Path.Value.StartsWith("/admin", StringComparison.OrdinalIgnoreCase)) + { + // если не указан frontend и это не первоначальная установка, тогда выводим ошибку + if (string.IsNullOrEmpty(AppInit.conf.listen.frontend) && File.Exists("module/manifest.json")) + return httpContext.Response.WriteAsync(unknownFrontend, httpContext.RequestAborted); + } + + var req = new RequestModel() + { + IsLocalRequest = IsLocalRequest, + IsLocalIp = IsLocalIp, + IP = clientIp, + Country = cf_country, + UserAgent = httpContext.Request.Headers.UserAgent + }; + + if (httpContext.Request.Headers.TryGetValue("X-Kit-AesGcm", out StringValues aesGcmKey) && aesGcmKey.Count > 0) + req.AesGcmKey = aesGcmKey; + + #region Weblog Request + if (!IsLocalRequest && !IsWsRequest && AppInit.conf.weblog.enable) + { + if (AppInit.conf.WebSocket.type == "signalr") + { + if (AppInit.conf.BaseModule.ws && soks.weblog_clients.Count > 0) + soks.SendLog(builderLog(httpContext, req), "request"); + } + else + { + if (AppInit.conf.BaseModule.nws && NativeWebSocket.weblog_clients.Count > 0) + NativeWebSocket.SendLog(builderLog(httpContext, req), "request"); + } + } + #endregion + + if (!string.IsNullOrEmpty(AppInit.conf.accsdb.domainId_pattern)) + { + string uid = Regex.Match(httpContext.Request.Host.Host, AppInit.conf.accsdb.domainId_pattern).Groups[1].Value; + req.user = AppInit.conf.accsdb.findUser(uid); + req.user_uid = uid; + + if (req.user == null) + return httpContext.Response.WriteAsync("user not found", httpContext.RequestAborted); + + req.@params = AppInit.conf.accsdb.@params; + + httpContext.Features.Set(req); + return _next(httpContext); + } + else + { + if (!IsWsRequest) + { + req.user = AppInit.conf.accsdb.findUser(httpContext, out string uid); + req.user_uid = uid; + + if (req.user != null) + req.@params = AppInit.conf.accsdb.@params; + + if (string.IsNullOrEmpty(req.user_uid)) + req.user_uid = getuid(httpContext); + } + + httpContext.Features.Set(req); + return _next(httpContext); + } + } + + + #region getuid + static readonly string[] uids = ["token", "account_email", "uid", "box_mac"]; + + static string getuid(HttpContext httpContext) + { + foreach (string id in uids) + { + if (httpContext.Request.Query.ContainsKey(id)) + { + StringValues val = httpContext.Request.Query[id]; + if (val.Count > 0 && IsValidUid(val[0])) + return val[0]; + } + } + + return null; + } + + static bool IsValidUid(ReadOnlySpan value) + { + if (value.IsEmpty) + return false; + + foreach (char ch in value) + { + if + ( + (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '_' || ch == '+' || ch == '.' || ch == '-' || ch == '@' || ch == '=' + ) + { + continue; + } + + return false; + } + + return true; + } + #endregion + + + static string builderLog(HttpContext httpContext, RequestModel req) + { + var logBuilder = new System.Text.StringBuilder(); + logBuilder.AppendLine($"{DateTime.Now}"); + logBuilder.AppendLine($"IP: {req.IP} {req.Country}"); + logBuilder.AppendLine($"URL: {AppInit.Host(httpContext)}{httpContext.Request.Path}{httpContext.Request.QueryString}\n"); + + foreach (var header in httpContext.Request.Headers) + logBuilder.AppendLine($"{header.Key}: {header.Value}"); + + return logBuilder.ToString(); + } + + + static readonly string unknownFrontend = @" + + + + + CloudFlare + + + +
      +
      +
      +
      Укажите frontend для правильной обработки запроса
      +
      +

      Добавьте в init.conf следующий код:

      +
      ""listen"": {
      +  ""frontend"": ""cloudflare""
      +}
      +
      +

      Либо отключите проверку CF-Connecting-IP:

      +
      ""listen"": {
      +  ""frontend"": ""off""
      +}
      +
      +

      Так же параметр можно изменить в админке: Остальное, base, frontend

      +
      +
      +
      + +"; + } + + + public class CounterRequestInfo + { + public int Value; + } +} diff --git a/Lampac/Engine/Middlewares/RequestStatistics.cs b/Lampac/Engine/Middlewares/RequestStatistics.cs new file mode 100644 index 0000000..a5cc7a9 --- /dev/null +++ b/Lampac/Engine/Middlewares/RequestStatistics.cs @@ -0,0 +1,170 @@ +using Microsoft.AspNetCore.Http; +using Shared; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class RequestStatistics + { + private readonly RequestDelegate _next; + + public RequestStatistics(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + if (!AppInit.conf.openstat.enable) + { + await _next(context); + return; + } + + Stopwatch stopwatch = RequestStatisticsTracker.StartRequest(); + + try + { + await _next(context); + } + finally + { + RequestStatisticsTracker.CompleteRequest(stopwatch); + } + } + } + + public static class RequestStatisticsTracker + { + static int activeHttpRequests; + static readonly ConcurrentQueue<(DateTime timestamp, double durationMs)> ResponseTimes = new(); + + static readonly Timer CleanupTimer = new Timer(CleanupResponseTimes, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + + internal static Stopwatch StartRequest() + { + Interlocked.Increment(ref activeHttpRequests); + return Stopwatch.StartNew(); + } + + internal static void CompleteRequest(Stopwatch stopwatch) + { + Interlocked.Decrement(ref activeHttpRequests); + + if (stopwatch == null) + return; + + stopwatch.Stop(); + AddResponseTime(stopwatch.Elapsed.TotalMilliseconds); + } + + static void AddResponseTime(double durationMs) + { + ResponseTimes.Enqueue((DateTime.UtcNow, durationMs)); + } + + static void CleanupResponseTimes(object state) + { + var cutoff = DateTime.UtcNow.AddSeconds(-60); + + while (ResponseTimes.TryPeek(out var oldest) && oldest.timestamp < cutoff) + ResponseTimes.TryDequeue(out _); + } + + + #region openstat + public static int ActiveHttpRequests => Volatile.Read(ref activeHttpRequests); + + public static ResponseTimeStatistics GetResponseTimeStatsLastMinute() + { + var now = DateTime.UtcNow; + + double sum = 0; + int count = 0; + var durations = new List(ResponseTimes.Count); + + foreach (var item in ResponseTimes) + { + sum += item.durationMs; + count++; + durations.Add(item.durationMs); + } + + if (count == 0) + { + return new ResponseTimeStatistics + { + Average = 0, + PercentileAverages = InitializePercentileDictionary() + }; + } + + durations.Sort(); + + return new ResponseTimeStatistics + { + Average = sum / count, + PercentileAverages = CalculatePercentileAverages(durations) + }; + } + + static Dictionary CalculatePercentileAverages(List sortedDurations) + { + const int bucketCount = 10; + var result = InitializePercentileDictionary(); + + int total = sortedDurations.Count; + int baseSize = total / bucketCount; + int remainder = total % bucketCount; + int currentIndex = 0; + + for (int i = 1; i <= bucketCount; i++) + { + int key = i * 10; + int bucketSize = baseSize + (i <= remainder ? 1 : 0); + + if (bucketSize > 0) + { + result[key] = AverageRange(sortedDurations, currentIndex, bucketSize); + currentIndex += bucketSize; + } + } + + return result; + } + + static Dictionary InitializePercentileDictionary() + { + var dict = new Dictionary(); + for (int i = 1; i <= 10; i++) + dict[i * 10] = 0; + + return dict; + } + + static double AverageRange(List sortedDurations, int startIndex, int length) + { + if (length <= 0) + return 0; + + double total = 0; + for (int i = 0; i < length; i++) + total += sortedDurations[startIndex + i]; + + return total / length; + } + + public class ResponseTimeStatistics + { + public double Average { get; set; } + + public Dictionary PercentileAverages { get; set; } = new(); + } + #endregion + } +} diff --git a/Lampac/Engine/Middlewares/Staticache.cs b/Lampac/Engine/Middlewares/Staticache.cs new file mode 100644 index 0000000..1a2846c --- /dev/null +++ b/Lampac/Engine/Middlewares/Staticache.cs @@ -0,0 +1,302 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Shared; +using Shared.Engine; +using Shared.Models; +using Shared.Models.AppConf; +using Shared.Models.Events; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Lampac.Engine.Middlewares +{ + public class Staticache + { + #region Staticache + protected static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); + + static readonly ConcurrentDictionary cacheFiles = new(); + + static readonly Timer cleanupTimer = new Timer(cleanup, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + + static Staticache() + { + Directory.CreateDirectory("cache/static"); + + var now = DateTime.Now; + + foreach (string inFile in Directory.EnumerateFiles("cache/static", "*")) + { + try + { + if (inFile.EndsWith(".gz")) + continue; + + // cacheKey-