mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-12 23:14:08 +02:00
Compare commits
142 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4bd7befb50 | ||
![]() |
56074155e9 | ||
![]() |
86a5a9cd25 | ||
![]() |
1792ed276e | ||
![]() |
4d1e4b59c8 | ||
![]() |
e1ff4b9e12 | ||
![]() |
86400e1b8a | ||
![]() |
b3d53d1c01 | ||
![]() |
471d494b3b | ||
![]() |
6e7d983fc3 | ||
![]() |
fd1073d0b5 | ||
![]() |
7815615112 | ||
![]() |
a6d8e68ca9 | ||
![]() |
9593ba2414 | ||
![]() |
8c95045a4a | ||
![]() |
667317e513 | ||
![]() |
0026a4f26e | ||
![]() |
2fac90df9f | ||
![]() |
424e7b773d | ||
![]() |
d25506e595 | ||
![]() |
a83e90e9ce | ||
![]() |
ea93182d96 | ||
![]() |
d60ee24111 | ||
![]() |
40e5c999b3 | ||
![]() |
6175b805d2 | ||
![]() |
5b6ba8d75f | ||
![]() |
10b0f0b1ce | ||
![]() |
a81ef63075 | ||
![]() |
2dc5edbb55 | ||
![]() |
636be458a6 | ||
![]() |
64283f137c | ||
![]() |
ec1c9a9461 | ||
![]() |
d9a806d236 | ||
![]() |
ebd294b602 | ||
![]() |
6081094b73 | ||
![]() |
115dfd20e7 | ||
![]() |
038fe2866b | ||
![]() |
aec40373d3 | ||
![]() |
c9b8d27139 | ||
![]() |
d0793ebcba | ||
![]() |
00e4e8109e | ||
![]() |
bcc1dc1948 | ||
![]() |
1e67996df0 | ||
![]() |
947bed0a46 | ||
![]() |
0dd9842e84 | ||
![]() |
42728e2694 | ||
![]() |
44d14dc19c | ||
![]() |
2e95c491a1 | ||
![]() |
acfc776462 | ||
![]() |
b6692a7dab | ||
![]() |
34fc2a0d15 | ||
![]() |
07938dcef8 | ||
![]() |
847c5dfdcb | ||
![]() |
bd47603f0d | ||
![]() |
8b86146715 | ||
![]() |
5343c9109b | ||
![]() |
687acae90b | ||
![]() |
871e540957 | ||
![]() |
b0ce0b5350 | ||
![]() |
bc965b1a0c | ||
![]() |
89b60c4c15 | ||
![]() |
47a84775c5 | ||
![]() |
8de0b0cbad | ||
![]() |
b95fbd76a7 | ||
![]() |
6b44cc9b74 | ||
![]() |
0612c70ca1 | ||
![]() |
060972475f | ||
![]() |
28bbeec462 | ||
![]() |
099adadbdc | ||
![]() |
43511c6ecb | ||
![]() |
aa3e9c6fde | ||
![]() |
e53cdcb909 | ||
![]() |
fd14ad3f93 | ||
![]() |
ca1af97e42 | ||
![]() |
4970219ea7 | ||
![]() |
54c5b66ecf | ||
![]() |
8ae09d453d | ||
![]() |
f2e7ec25c8 | ||
![]() |
7f9fff4683 | ||
![]() |
13f2eea298 | ||
![]() |
d6f5a1b242 | ||
![]() |
ab37f0c2c7 | ||
![]() |
9710b37064 | ||
![]() |
54f7568111 | ||
![]() |
06758964c0 | ||
![]() |
b56552e0f4 | ||
![]() |
ef8653f7d2 | ||
![]() |
d5a6987390 | ||
![]() |
8fb92ca05c | ||
![]() |
11c7015c17 | ||
![]() |
8bc5d813b7 | ||
![]() |
012f5f05cc | ||
![]() |
cd65ec7a71 | ||
![]() |
45826df4fe | ||
![]() |
0645bab613 | ||
![]() |
4e409dfb50 | ||
![]() |
52a39b7a71 | ||
![]() |
5278b9fb47 | ||
![]() |
1ecafe3667 | ||
![]() |
190a415907 | ||
![]() |
ca609a9e62 | ||
![]() |
b72ade7b27 | ||
![]() |
e7098d01c5 | ||
![]() |
b04ee56612 | ||
![]() |
72a18e9b73 | ||
![]() |
6be64bbe36 | ||
![]() |
1a14e8dd4b | ||
![]() |
a25eb7f951 | ||
![]() |
9437cd54d3 | ||
![]() |
be55c5e4a6 | ||
![]() |
68351a480b | ||
![]() |
33e35f1bd3 | ||
![]() |
e82b54994e | ||
![]() |
211e152863 | ||
![]() |
5818b914a3 | ||
![]() |
0680d39d90 | ||
![]() |
4ca6676be0 | ||
![]() |
3d85e751b7 | ||
![]() |
965da06214 | ||
![]() |
9abce7a586 | ||
![]() |
53162b4bd3 | ||
![]() |
80d78a027b | ||
![]() |
3843d68766 | ||
![]() |
23f55ef33a | ||
![]() |
e4c729a588 | ||
![]() |
f09c5722be | ||
![]() |
e73ae99e38 | ||
![]() |
10c5c99385 | ||
![]() |
0a70f91bf1 | ||
![]() |
88945be5d7 | ||
![]() |
5df131140f | ||
![]() |
8002483e68 | ||
![]() |
ce84264490 | ||
![]() |
615794a4bf | ||
![]() |
5eed9d7038 | ||
![]() |
f162902b36 | ||
![]() |
a752683965 | ||
![]() |
683bd6e2d4 | ||
![]() |
13426fe7ec | ||
![]() |
d87d0f87b1 | ||
![]() |
85ec169755 | ||
![]() |
9cc40144a9 |
BIN
.github/cookies-chrome.png
vendored
Normal file
BIN
.github/cookies-chrome.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 480 KiB |
BIN
.github/cookies-firefox.png
vendored
Normal file
BIN
.github/cookies-firefox.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 439 KiB |
@@ -2,8 +2,8 @@
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Jackett"
|
||||
#define MyAppVersion GetFileVersion("BuildOutput\FullFramework\Windows\Jackett\Jackett.Common.dll")
|
||||
#define MyAppPublisher "Jackett Inc."
|
||||
#define MyAppVersion GetFileVersion(MyFileForVersion)
|
||||
#define MyAppPublisher "Jackett"
|
||||
#define MyAppURL "https://github.com/Jackett/Jackett"
|
||||
#define MyAppExeName "JackettTray.exe"
|
||||
|
||||
@@ -22,9 +22,11 @@ AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={pf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename=Jackett.Installer.Windows
|
||||
SetupIconFile=src\Jackett.Console\jackett.ico
|
||||
UninstallDisplayIcon={commonappdata}\Jackett\JackettConsole.exe
|
||||
OutputBaseFilename={#MyOutputFilename}
|
||||
SetupIconFile=src\Jackett.Tray\jackett.ico
|
||||
UninstallDisplayIcon={commonappdata}\Jackett\{#MyAppExeName}
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
UninstallDisplayName={#MyAppName}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
DisableDirPage=yes
|
||||
@@ -37,9 +39,7 @@ Name: "windowsService"; Description: "Install as a Windows Service"
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "BuildOutput\FullFramework\Windows\Jackett\JackettTray.exe"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion
|
||||
Source: "BuildOutput\FullFramework\Windows\Jackett\JackettUpdater.exe"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion
|
||||
Source: "BuildOutput\FullFramework\Windows\Jackett\*"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#MySourceFolder}\*"; DestDir: "{commonappdata}\Jackett"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
@@ -48,16 +48,15 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
|
||||
Name: "{commondesktop}\{#MyAppName}"; Filename: "{commonappdata}\Jackett\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Uninstall"; Flags: waituntilterminated runhidden;
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--ReserveUrls"; Flags: waituntilterminated runhidden;
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Install"; Flags: waituntilterminated runhidden; Tasks: windowsService
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Start"; Flags: waituntilterminated runhidden; Tasks: windowsService
|
||||
Filename: "{commonappdata}\Jackett\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Run]
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Uninstall"; Flags: waituntilterminated;
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--ReserveUrls"; Flags: waituntilterminated;
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--MigrateSettings"; Flags: waituntilterminated;
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Install"; Flags: waituntilterminated; Tasks: windowsService
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Start"; Flags: waituntilterminated; Tasks: windowsService
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Uninstall"; Flags: waituntilterminated skipifdoesntexist
|
||||
Filename: "{commonappdata}\Jackett\JackettConsole.exe"; Parameters: "--Uninstall"; Flags: waituntilterminated skipifdoesntexist runhidden
|
||||
Filename: "{sys}\taskkill.exe"; Parameters: "/f /im {#MyAppExeName}"; Flags: waituntilterminated skipifdoesntexist runhidden
|
||||
Filename: "{sys}\taskkill.exe"; Parameters: "/f /im JackettConsole.exe"; Flags: waituntilterminated skipifdoesntexist runhidden
|
||||
|
||||
|
||||
|
@@ -17,7 +17,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
|
||||
#### Supported Systems
|
||||
* Windows using .NET 4.6.1 or above [Download here](https://www.microsoft.com/net/framework/versions/net461).
|
||||
* Linux and macOS using Mono 5.8 or above. [Download here](http://www.mono-project.com/download/). Earlier versions of mono may work, but some trackers may fail to negotiate SSL correctly, and others may cause Jackett to crash when used.
|
||||
* Linux and macOS using Mono 5.8 or above. [Download here](http://www.mono-project.com/download/).
|
||||
|
||||
### Supported Public Trackers
|
||||
* 1337x
|
||||
@@ -42,6 +42,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* KickAssTorrent (thekat.se clone)
|
||||
* LimeTorrents
|
||||
* MagnetDL
|
||||
* MejorTorrent <!-- maintained by ivandelabeldad -->
|
||||
* NextTorrent
|
||||
* Newpct (aka: tvsinpagar, descargas2020, torrentlocura, torrentrapid, etc)
|
||||
* Nyaa.si
|
||||
@@ -125,7 +126,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* BroadcastTheNet
|
||||
* BrokenStones
|
||||
* BTNext
|
||||
* BTWorld
|
||||
* BTXpress
|
||||
* Carpathians
|
||||
* CCFBits
|
||||
* CGPeers
|
||||
@@ -243,6 +244,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* Superbits
|
||||
* Tasmanit
|
||||
* TBPlus
|
||||
* TehConnection.me
|
||||
* TenYardTracker
|
||||
* The Empire
|
||||
* The Geeks
|
||||
@@ -336,7 +338,7 @@ Mono must be compiled with the Roslyn compiler (default), using MCS will cause "
|
||||
## Installation on macOS
|
||||
|
||||
### Prerequisites
|
||||
Install [Mono 4.6](http://www.mono-project.com/download/#download-mac) or better (using the latest pkg installer is recommended).
|
||||
Install [Mono 5.8](http://www.mono-project.com/download/#download-mac) or better (using the latest pkg installer is recommended).
|
||||
* Setup ssl support by running `curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync --user /dev/stdin`
|
||||
|
||||
### Install as service
|
||||
@@ -402,6 +404,7 @@ All contributions are welcome just send a pull request. Jackett's framework all
|
||||
## Building from source
|
||||
|
||||
### Windows
|
||||
* Install the .NET Core [SDK](https://www.microsoft.com/net/download/windows)
|
||||
* Open the Jackett solution in Visual Studio 2017 (version 15.7 or above)
|
||||
* Right click on the Jackett solution and click 'Rebuild Solution' to restore nuget packages
|
||||
* Select Jackett.Console as startup project
|
||||
|
@@ -1,4 +1,4 @@
|
||||
version: 0.8.{build}
|
||||
version: 0.9.{build}
|
||||
skip_tags: true
|
||||
image: Visual Studio 2017
|
||||
configuration: Release
|
||||
|
170
build.cake
170
build.cake
@@ -6,7 +6,7 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
var target = Argument("target", "Default");
|
||||
var configuration = Argument("configuration", "Release");
|
||||
var configuration = Argument("configuration", "Debug");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// PREPARATION
|
||||
@@ -36,8 +36,8 @@ Task("Clean")
|
||||
.IsDependentOn("Info")
|
||||
.Does(() =>
|
||||
{
|
||||
CleanDirectories("./src/**/obj" + configuration);
|
||||
CleanDirectories("./src/**/bin" + configuration);
|
||||
CleanDirectories("./src/**/obj");
|
||||
CleanDirectories("./src/**/bin");
|
||||
CleanDirectories("./BuildOutput");
|
||||
CleanDirectories("./" + artifactsDirName);
|
||||
CleanDirectories("./" + testResultsDirName);
|
||||
@@ -56,7 +56,11 @@ Task("Build")
|
||||
.IsDependentOn("Restore-NuGet-Packages")
|
||||
.Does(() =>
|
||||
{
|
||||
MSBuild("./src/Jackett.sln", settings => settings.SetConfiguration(configuration));
|
||||
var buildSettings = new MSBuildSettings()
|
||||
.SetConfiguration(configuration)
|
||||
.UseToolVersion(MSBuildToolVersion.VS2017);
|
||||
|
||||
MSBuild("./src/Jackett.sln", buildSettings);
|
||||
});
|
||||
|
||||
Task("Run-Unit-Tests")
|
||||
@@ -84,9 +88,7 @@ Task("Copy-Files-Full-Framework")
|
||||
var windowsOutput = windowsBuildFullFramework + "/Jackett";
|
||||
|
||||
CopyDirectory("./src/Jackett.Console/bin/" + configuration, windowsOutput);
|
||||
CopyFiles("./src/Jackett.Service/bin/" + configuration + "/JackettService.*", windowsOutput);
|
||||
CopyFiles("./src/Jackett.Tray/bin/" + configuration + "/JackettTray.*", windowsOutput);
|
||||
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/JackettUpdater.*", windowsOutput);
|
||||
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net452" + "/JackettUpdater.*", windowsOutput); //builds against multiple frameworks
|
||||
CopyFiles("./Upstart.config", windowsOutput);
|
||||
CopyFiles("./LICENSE", windowsOutput);
|
||||
CopyFiles("./README.md", windowsOutput);
|
||||
@@ -115,49 +117,118 @@ Task("Check-Packaging-Platform")
|
||||
}
|
||||
});
|
||||
|
||||
Task("Package-Windows-Installer-Full-Framework")
|
||||
.IsDependentOn("Check-Packaging-Platform")
|
||||
.Does(() =>
|
||||
{
|
||||
InnoSetup("./Installer.iss", new InnoSetupSettings {
|
||||
OutputDirectory = workingDir + "/" + artifactsDirName
|
||||
});
|
||||
});
|
||||
|
||||
Task("Package-Files-Full-Framework-Windows")
|
||||
.IsDependentOn("Check-Packaging-Platform")
|
||||
.Does(() =>
|
||||
{
|
||||
Zip(windowsBuildFullFramework, $"./{artifactsDirName}/Jackett.Binaries.Windows.zip");
|
||||
Information(@"Full Framework Windows Binaries Zipping Completed");
|
||||
});
|
||||
|
||||
Task("Package-Files-Full-Framework-Mono")
|
||||
.IsDependentOn("Check-Packaging-Platform")
|
||||
.Does(() =>
|
||||
{
|
||||
var cygMonoBuildPath = RelativeWinPathToCygPath(monoBuildFullFramework);
|
||||
var tarFileName = "Jackett.Binaries.Mono.tar";
|
||||
var tarArguments = @"-cvf " + cygMonoBuildPath + "/" + tarFileName + " -C " + cygMonoBuildPath + " Jackett --mode ='755'";
|
||||
var gzipArguments = @"-k " + cygMonoBuildPath + "/" + tarFileName;
|
||||
|
||||
RunCygwinCommand("Tar", tarArguments);
|
||||
RunCygwinCommand("Gzip", gzipArguments);
|
||||
|
||||
MoveFile($"{monoBuildFullFramework}/{tarFileName}.gz", $"./{artifactsDirName}/{tarFileName}.gz");
|
||||
Gzip(monoBuildFullFramework, $"./{artifactsDirName}", "Jackett", "Jackett.Binaries.Mono.tar.gz");
|
||||
Information(@"Full Framework Mono Binaries Gzip Completed");
|
||||
});
|
||||
|
||||
Task("Package-Full-Framework")
|
||||
.IsDependentOn("Package-Windows-Installer-Full-Framework")
|
||||
.IsDependentOn("Package-Files-Full-Framework-Windows")
|
||||
.IsDependentOn("Package-Files-Full-Framework-Mono")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Full Framwork Packaging Completed");
|
||||
Information("Full Framework Packaging Completed");
|
||||
});
|
||||
|
||||
Task("Kestrel-Full-Framework")
|
||||
.IsDependentOn("Package-Full-Framework")
|
||||
.Does(() =>
|
||||
{
|
||||
CleanDirectories("./src/**/obj");
|
||||
CleanDirectories("./src/**/bin");
|
||||
|
||||
NuGetRestore("./src/Jackett.sln");
|
||||
|
||||
var buildSettings = new MSBuildSettings()
|
||||
.SetConfiguration(configuration)
|
||||
.UseToolVersion(MSBuildToolVersion.VS2017);
|
||||
|
||||
MSBuild("./src/Jackett.sln", buildSettings);
|
||||
});
|
||||
|
||||
Task("Experimental-Kestrel-Windows-Full-Framework")
|
||||
.IsDependentOn("Kestrel-Full-Framework")
|
||||
.Does(() =>
|
||||
{
|
||||
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
|
||||
string buildOutputPath = "./BuildOutput/Experimental/net461/win7-x86/Jackett";
|
||||
|
||||
DotNetCorePublish(serverProjectPath, "net461", "win7-x86");
|
||||
|
||||
CopyFiles("./src/Jackett.Service/bin/" + configuration + "/JackettService.*", buildOutputPath);
|
||||
CopyFiles("./src/Jackett.Tray/bin/" + configuration + "/JackettTray.*", buildOutputPath);
|
||||
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net461" + "/JackettUpdater.*", buildOutputPath); //builds against multiple frameworks
|
||||
|
||||
Zip("./BuildOutput/Experimental/net461/win7-x86", $"./{artifactsDirName}/Jackett.Binaries.Windows.zip");
|
||||
|
||||
//InnoSetup
|
||||
string sourceFolder = MakeAbsolute(Directory(buildOutputPath)).ToString();
|
||||
|
||||
InnoSetupSettings settings = new InnoSetupSettings();
|
||||
settings.OutputDirectory = workingDir + "/" + artifactsDirName;
|
||||
settings.Defines = new Dictionary<string, string>
|
||||
{
|
||||
{ "MyFileForVersion", sourceFolder + "/Jackett.Common.dll" },
|
||||
{ "MySourceFolder", sourceFolder },
|
||||
{ "MyOutputFilename", "Jackett.Installer.Windows" },
|
||||
};
|
||||
|
||||
InnoSetup("./Installer.iss", settings);
|
||||
});
|
||||
|
||||
Task("Experimental-Kestrel-Mono-Full-Framework")
|
||||
.IsDependentOn("Kestrel-Full-Framework")
|
||||
.Does(() =>
|
||||
{
|
||||
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
|
||||
string buildOutputPath = "./BuildOutput/Experimental/net461/linux-x64/Jackett";
|
||||
|
||||
DotNetCorePublish(serverProjectPath, "net461", "linux-x64");
|
||||
|
||||
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net461" + "/JackettUpdater.*", buildOutputPath); //builds against multiple frameworks
|
||||
|
||||
//There is an issue with Mono 5.8 (fixed in Mono 5.12) where its expecting to use its own patched version of System.Net.Http.dll, instead of the version supplied in folder
|
||||
//https://github.com/dotnet/corefx/issues/19914
|
||||
//https://bugzilla.xamarin.com/show_bug.cgi?id=60315
|
||||
//The workaround is to delete System.Net.Http.dll and patch the .exe.config file
|
||||
|
||||
DeleteFile(buildOutputPath + "/System.Net.Http.dll");
|
||||
|
||||
var configFile = File(buildOutputPath + "/JackettConsole.exe.config");
|
||||
XmlPoke(configFile, "configuration/runtime/*[name()='assemblyBinding']/*[name()='dependentAssembly']/*[name()='assemblyIdentity'][@name='System.Net.Http']/../*[name()='bindingRedirect']/@newVersion", "4.0.0.0");
|
||||
|
||||
Gzip("./BuildOutput/Experimental/net461/linux-x64", $"./{artifactsDirName}", "Jackett", "Experimental.Jackett.Binaries.Mono.tar.gz");
|
||||
});
|
||||
|
||||
Task("Experimental-DotNetCore")
|
||||
.IsDependentOn("Kestrel-Full-Framework")
|
||||
.Does(() =>
|
||||
{
|
||||
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
|
||||
|
||||
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "win-x86");
|
||||
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "linux-x64");
|
||||
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "osx-x64");
|
||||
|
||||
Zip("./BuildOutput/Experimental/netcoreapp2.1/win-x86", $"./{artifactsDirName}/Experimental.netcoreapp.win-x86.zip");
|
||||
Zip("./BuildOutput/Experimental/netcoreapp2.1/osx-x64", $"./{artifactsDirName}/Experimental.netcoreapp.osx-x64.zip");
|
||||
Gzip("./BuildOutput/Experimental/netcoreapp2.1/linux-x64", $"./{artifactsDirName}", "Jackett", "Experimental.netcoreapp.linux-x64.tar.gz");
|
||||
});
|
||||
|
||||
Task("Experimental")
|
||||
.IsDependentOn("Experimental-Kestrel-Windows-Full-Framework")
|
||||
.IsDependentOn("Experimental-Kestrel-Mono-Full-Framework")
|
||||
//.IsDependentOn("Experimental-DotNetCore")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Experimental builds completed");
|
||||
});
|
||||
|
||||
Task("Appveyor-Push-Artifacts")
|
||||
.IsDependentOn("Package-Full-Framework")
|
||||
.IsDependentOn("Experimental")
|
||||
.Does(() =>
|
||||
{
|
||||
if (AppVeyor.IsRunningOnAppVeyor)
|
||||
@@ -263,10 +334,35 @@ private void RunCygwinCommand(string utility, string utilityArguments)
|
||||
private string RelativeWinPathToCygPath(string relativePath)
|
||||
{
|
||||
var cygdriveBase = "/cygdrive/" + workingDir.ToString().Replace(":", "").Replace("\\", "/");
|
||||
var cygPath = cygdriveBase + relativePath.Replace(".", "");
|
||||
var cygPath = cygdriveBase + relativePath.TrimStart('.');
|
||||
return cygPath;
|
||||
}
|
||||
|
||||
private void Gzip(string sourceFolder, string outputDirectory, string tarCdirectoryOption, string outputFileName)
|
||||
{
|
||||
var cygSourcePath = RelativeWinPathToCygPath(sourceFolder);
|
||||
var tarFileName = outputFileName.Remove(outputFileName.Length - 3, 3);
|
||||
var tarArguments = @"-cvf " + cygSourcePath + "/" + tarFileName + " -C " + cygSourcePath + $" {tarCdirectoryOption} --mode ='755'";
|
||||
var gzipArguments = @"-k " + cygSourcePath + "/" + tarFileName;
|
||||
|
||||
RunCygwinCommand("Tar", tarArguments);
|
||||
RunCygwinCommand("Gzip", gzipArguments);
|
||||
|
||||
MoveFile($"{sourceFolder}/{tarFileName}.gz", $"{outputDirectory}/{tarFileName}.gz");
|
||||
}
|
||||
|
||||
private void DotNetCorePublish(string projectPath, string framework, string runtime)
|
||||
{
|
||||
var settings = new DotNetCorePublishSettings
|
||||
{
|
||||
Framework = framework,
|
||||
Runtime = runtime,
|
||||
OutputDirectory = $"./BuildOutput/Experimental/{framework}/{runtime}/Jackett"
|
||||
};
|
||||
|
||||
DotNetCorePublish(projectPath, settings);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// TASK TARGETS
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
@@ -87,6 +87,7 @@ function loadJackettSettings() {
|
||||
$("#jackett-prerelease").attr('checked', data.prerelease);
|
||||
$("#jackett-logging").attr('checked', data.logging);
|
||||
$("#jackett-omdbkey").val(data.omdbkey);
|
||||
$("#jackett-omdburl").val(data.omdburl);
|
||||
var password = data.password;
|
||||
$("#jackett-adminpwd").val(password);
|
||||
if (password != null && password != '') {
|
||||
@@ -1150,6 +1151,7 @@ function bindUIButtons() {
|
||||
var jackett_prerelease = $("#jackett-prerelease").is(':checked');
|
||||
var jackett_logging = $("#jackett-logging").is(':checked');
|
||||
var jackett_omdb_key = $("#jackett-omdbkey").val();
|
||||
var jackett_omdb_url = $("#jackett-omdburl").val();
|
||||
|
||||
var jackett_proxy_url = $("#jackett-proxy-url").val();
|
||||
var jackett_proxy_type = $("#jackett-proxy-type").val();
|
||||
@@ -1166,6 +1168,7 @@ function bindUIButtons() {
|
||||
logging: jackett_logging,
|
||||
basepathoverride: jackett_basepathoverride,
|
||||
omdbkey: jackett_omdb_key,
|
||||
omdburl: jackett_omdb_url,
|
||||
proxy_type: jackett_proxy_type,
|
||||
proxy_url: jackett_proxy_url,
|
||||
proxy_port: jackett_proxy_port,
|
||||
|
@@ -177,6 +177,10 @@
|
||||
<span class="input-header">OMDB API key: </span>
|
||||
<input id="jackett-omdbkey" class="form-control input-right" type="text" value="" placeholder="">
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<span class="input-header">OMDB API Url: </span>
|
||||
<input id="jackett-omdburl" class="form-control input-right" type="text" value="" placeholder="Blank for default">
|
||||
</div>
|
||||
<hr />
|
||||
<div id="footer">
|
||||
<a href="https://github.com/Jackett/Jackett" target="_blank" title="Jackett on GitHub">Jackett</a> Version <span id="app-version"></span>
|
||||
@@ -659,6 +663,6 @@
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="../libs/api.js?changed=2017083001"></script>
|
||||
<script type="text/javascript" src="../custom.js?changed=2017110603"></script>
|
||||
<script type="text/javascript" src="../custom.js?changed=20180709"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -40,6 +40,18 @@
|
||||
search: [q]
|
||||
tv-search: [q, season, ep]
|
||||
movie-search: [q]
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: info
|
||||
type: info
|
||||
label: Results Per Page
|
||||
default: For best results, change the 'Torrentliste' setting to "Platzsparendes Layout mit PopUp für zusätzliche Informationen" in your profile.
|
||||
|
||||
login:
|
||||
path: takelogin.php
|
||||
@@ -63,9 +75,13 @@
|
||||
|
||||
rows:
|
||||
selector: table.tableinborder > tbody > tr:has(a[href^="details.php"])
|
||||
fields:
|
||||
fields: # note: two alternative layouts available
|
||||
title:
|
||||
selector: a[href^="details.php"]
|
||||
title:
|
||||
optional: true
|
||||
selector: a[href^="details.php"][title]
|
||||
attribute: title
|
||||
category:
|
||||
selector: a[href^="browse.php?cat="]
|
||||
attribute: href
|
||||
@@ -79,29 +95,31 @@
|
||||
selector: a[href^=" /gettorrent/ssl/"]
|
||||
attribute: href
|
||||
files:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(2)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(2), a[href*="&filelist=1"]
|
||||
grabs:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(3) > b:nth-child(1)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(3) > b:nth-child(1), a[href*="&tosnatchers=1"]
|
||||
size:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(1)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(1), td:nth-child(7):has(br)
|
||||
filters:
|
||||
- name: replace
|
||||
args: [".", ""]
|
||||
- name: replace
|
||||
args: [",", "."]
|
||||
seeders:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(1)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(1), a[href*="&toseeders=1"]
|
||||
leechers:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(3)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(3), a[href*="&todlers=1"]
|
||||
date:
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(5)
|
||||
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(5), td:nth-child(5):has(br)
|
||||
filters:
|
||||
- name: replace
|
||||
args: [" ", ""]
|
||||
- name: append
|
||||
args: " +2:00"
|
||||
- name: replace
|
||||
args: ["\xA0", " "]
|
||||
args: ["\xA0", ""]
|
||||
- name: dateparse
|
||||
args: "02.01.2006 15:04:05 -07:00"
|
||||
args: "02.01.200615:04:05 -07:00"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
img[src="/pic/free.gif"]: "0"
|
||||
|
@@ -44,15 +44,8 @@
|
||||
sorting: created_at
|
||||
direction: desc
|
||||
qty: 100
|
||||
preprocessingfilters:
|
||||
- name: jsonjoinarray
|
||||
args: ["$.result", ""]
|
||||
- name: prepend
|
||||
args: "<table>"
|
||||
- name: append
|
||||
args: "</table>"
|
||||
rows:
|
||||
selector: tr
|
||||
selector: table > tbody > tr
|
||||
fields:
|
||||
category:
|
||||
selector: a[href*="/categories/"]
|
||||
@@ -63,37 +56,24 @@
|
||||
title:
|
||||
selector: a.view-torrent
|
||||
download:
|
||||
selector: a[href*="/download_check/"]
|
||||
selector: a[href*="/download/"]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["/download_check/", "/download/"]
|
||||
details:
|
||||
selector: a.view-torrent
|
||||
attribute: href
|
||||
imdb:
|
||||
optional: true
|
||||
selector: a[href*="://www.imdb.com/title/"]
|
||||
attribute: href
|
||||
size:
|
||||
selector: td:nth-child(5)
|
||||
selector: td:nth-child(4)
|
||||
seeders:
|
||||
selector: td:nth-child(7)
|
||||
leechers:
|
||||
selector: td:nth-child(8)
|
||||
grabs:
|
||||
selector: td:nth-child(6)
|
||||
leechers:
|
||||
selector: td:nth-child(7)
|
||||
grabs:
|
||||
selector: td:nth-child(5)
|
||||
filters:
|
||||
- name: regexp
|
||||
args: ([\d\.]+)
|
||||
date:
|
||||
selector: time
|
||||
attribute: datetime
|
||||
filters:
|
||||
- name: append
|
||||
args: " +00:00"
|
||||
- name: dateparse
|
||||
args: "2006-01-02 15:04:05 -07:00"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"i[data-original-title=\"100% Free\"]": "0"
|
||||
|
@@ -1,23 +1,24 @@
|
||||
---
|
||||
site: btworld
|
||||
name: BTWorld
|
||||
description: "HD Movie/TV Tracker"
|
||||
site: btxpress
|
||||
name: BTXpress
|
||||
description: "HD Tracker Movies/TV/Music"
|
||||
language: en-us
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://btworld.org/
|
||||
|
||||
- https://btxpress.org/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 1, cat: Movies, desc: "Movies"}
|
||||
- {id: 2, cat: TV, desc: "TV"}
|
||||
|
||||
- {id: 3, cat: Audio, desc: "Music"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q, season, ep, imdbid]
|
||||
movie-search: [q, imdbid]
|
||||
|
||||
|
||||
login:
|
||||
path: /login
|
||||
method: form
|
||||
@@ -28,13 +29,14 @@
|
||||
- selector: table.main:contains("Login Failed!")
|
||||
test:
|
||||
path: /torrents
|
||||
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: /filterTorrents
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}categories[]={{.}}&{{end}}"
|
||||
search: "{{if .Query.IMDBID}}{{else}}{{ .Keywords }}{{end}}"
|
||||
description: ""
|
||||
uploader: ""
|
||||
imdb: "{{ .Query.IMDBIDShort }}"
|
||||
tvdb: ""
|
||||
@@ -70,10 +72,6 @@
|
||||
details:
|
||||
selector: a.view-torrent
|
||||
attribute: href
|
||||
imdb:
|
||||
optional: true
|
||||
selector: a[href*="://www.imdb.com/title/"]
|
||||
attribute: href
|
||||
size:
|
||||
selector: td:nth-child(5)
|
||||
seeders:
|
@@ -45,10 +45,7 @@
|
||||
- selector: table:contains("Login failed!")
|
||||
test:
|
||||
path: index.php
|
||||
|
||||
download:
|
||||
selector: a[href^="download.php?id="]
|
||||
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: browse.php
|
||||
|
@@ -6,8 +6,9 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://www.cpasbiens.cc/
|
||||
- http://www.cpabien.io/
|
||||
legacylinks:
|
||||
- http://www.cpasbiens.cc/
|
||||
- http://www.cpabien.cm/
|
||||
- http://cpabien.cm/
|
||||
- http://cpasbiens1.com/
|
||||
|
@@ -207,23 +207,23 @@
|
||||
- name: querystring
|
||||
args: "category"
|
||||
date:
|
||||
selector: td:nth-of-type(4)
|
||||
selector: td:nth-of-type(5)
|
||||
filters:
|
||||
- name: append
|
||||
args: " -04:00"
|
||||
- name: dateparse
|
||||
args: "02/01/2006 15:04:05 -07:00"
|
||||
seeders:
|
||||
selector: td:nth-of-type(6)
|
||||
leechers:
|
||||
selector: td:nth-of-type(7)
|
||||
grabs:
|
||||
leechers:
|
||||
selector: td:nth-of-type(8)
|
||||
grabs:
|
||||
selector: td:nth-of-type(9)
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["---", "0"]
|
||||
size:
|
||||
selector: td:nth-of-type(10)
|
||||
selector: td:nth-of-type(11)
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
img[src="images/freeleech.gif"]: "0"
|
||||
|
@@ -47,11 +47,33 @@
|
||||
paths:
|
||||
- path: "{{ if .Keywords }}buscar/descargas/{{ .Config.category }}/{{ .Keywords }}?search=Buscar{{else}}descargas{{end}}"
|
||||
- path: "{{ if .Keywords }}buscar/descargas/{{ .Config.category }}/{{ .Keywords }}?search=Buscar&page=2{{else}}descargas{{end}}"
|
||||
keywordsfilters:
|
||||
- name: re_replace #remove S/EXX from search string
|
||||
args: ["([SE]\\d{1,2})", ""]
|
||||
rows:
|
||||
selector: table#descargas > tbody > tr:has(td:has(a[href^="magnet:?"]))
|
||||
fields:
|
||||
title:
|
||||
selector: td.tit a
|
||||
filters:
|
||||
- name: re_replace
|
||||
args: ["\\/", " "]
|
||||
- name: re_replace
|
||||
args: ["\\(", ""]
|
||||
- name: re_replace
|
||||
args: ["\\)", ""]
|
||||
- name: re_replace
|
||||
args: ["([A-z]*) temporada", "S$1"]
|
||||
- name: re_replace
|
||||
args: ["S[pP]rimera", "S1"]
|
||||
- name: re_replace
|
||||
args: ["S[sS]egunda", "S2"]
|
||||
- name: re_replace
|
||||
args: ["S[tT]ercera", "S3"]
|
||||
- name: re_replace
|
||||
args: ["S([0-9]+) - Episodio ([0-9]+)", "S$1E$2"]
|
||||
- name: re_replace
|
||||
args: ["- Episodio ([0-9]*)", "E$1"]
|
||||
details:
|
||||
selector: td.tit a
|
||||
attribute: href
|
||||
|
@@ -6,8 +6,9 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://www.gktorrent.net/
|
||||
- https://www.gktorrent.org/
|
||||
legacylinks:
|
||||
- http://www.gktorrent.net/
|
||||
- https://www.gktorrent.com/ # they're forcing http
|
||||
- http://www.gktorrent.com/
|
||||
|
||||
|
@@ -9,6 +9,7 @@
|
||||
- https://ilcorsaronero.info/
|
||||
certificates:
|
||||
- aa7c40aa360a1cec8a9687312fd50402b912e618 # incomplete CA chain
|
||||
- 83174ec1f92fa13cdef9d51888ea1dfba2166e17 # incomplete CA chain
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
|
@@ -82,6 +82,8 @@
|
||||
|
||||
rows:
|
||||
selector: tr.browse_color
|
||||
filters:
|
||||
- name: andmatch
|
||||
fields:
|
||||
category:
|
||||
selector: td:nth-of-type(1) a
|
||||
|
@@ -38,7 +38,7 @@
|
||||
- path: /torrents/search.html
|
||||
method: post
|
||||
inputs:
|
||||
"SearchTorrentsForm[nameTorrent]": "{{ .Keywords }}"
|
||||
"SearchTorrentsForm[nameTorrent]": "{{if .Query.Artist}}{{ .Query.Artist }}{{else}}{{ .Keywords }}{{end}}"
|
||||
go-search: "Search"
|
||||
rows:
|
||||
selector: .smallalbum
|
||||
|
@@ -45,6 +45,17 @@
|
||||
search: [q]
|
||||
tv-search: [q, season, ep]
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: pin
|
||||
type: text
|
||||
label: Pin
|
||||
|
||||
login:
|
||||
path: /login.php
|
||||
method: form
|
||||
@@ -52,6 +63,7 @@
|
||||
inputs:
|
||||
username: "{{ .Config.username }}"
|
||||
password: "{{ .Config.password }}"
|
||||
secure_pin: "{{ .Config.pin }}"
|
||||
returnto: "/"
|
||||
error:
|
||||
- selector: table.tableinborder:contains("Anmeldung Gescheitert!") > tbody > tr > td.tablea
|
||||
|
@@ -54,15 +54,8 @@
|
||||
sorting: created_at
|
||||
direction: desc
|
||||
qty: 100
|
||||
preprocessingfilters:
|
||||
- name: jsonjoinarray
|
||||
args: ["$.result", ""]
|
||||
- name: prepend
|
||||
args: "<table>"
|
||||
- name: append
|
||||
args: "</table>"
|
||||
rows:
|
||||
selector: tr
|
||||
selector: table > tbody > tr
|
||||
fields:
|
||||
category:
|
||||
selector: a[href*="/categories/"]
|
||||
@@ -73,33 +66,24 @@
|
||||
title:
|
||||
selector: a.view-torrent
|
||||
download:
|
||||
selector: a[href*="/download_check/"]
|
||||
selector: a[href*="/download/"]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["/download_check/", "/download/"]
|
||||
details:
|
||||
selector: a.view-torrent
|
||||
attribute: href
|
||||
size:
|
||||
selector: td:nth-child(5)
|
||||
selector: td:nth-child(4)
|
||||
seeders:
|
||||
selector: td:nth-child(7)
|
||||
leechers:
|
||||
selector: td:nth-child(8)
|
||||
grabs:
|
||||
selector: td:nth-child(6)
|
||||
leechers:
|
||||
selector: td:nth-child(7)
|
||||
grabs:
|
||||
selector: td:nth-child(5)
|
||||
filters:
|
||||
- name: regexp
|
||||
args: ([\d\.]+)
|
||||
date:
|
||||
selector: time
|
||||
attribute: datetime
|
||||
filters:
|
||||
- name: append
|
||||
args: " +00:00"
|
||||
- name: dateparse
|
||||
args: "2006-01-02 15:04:05 -07:00"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"i[data-original-title=\"100% Free\"]": "0"
|
||||
|
@@ -31,33 +31,32 @@
|
||||
- {id: 61, cat: Books, desc: "Audiolibri"}
|
||||
# Games
|
||||
- {id: 47, cat: PC/Games, desc: "Games PC"}
|
||||
- {id: 22, cat: Console/Other, desc: "Nintendo"}
|
||||
- {id: 40, cat: Console/Other, desc: "Nintendo"}
|
||||
- {id: 13, cat: Console/PS4, desc: "Sony PS"}
|
||||
- {id: 20, cat: Console/Xbox, desc: "XboX"}
|
||||
- {id: 33, cat: Console/Xbox, desc: "XboX"}
|
||||
- {id: 14, cat: Console/Wii, desc: "Wii"}
|
||||
# Music
|
||||
- {id: 54, cat: Audio/MP3, desc: "MP3"}
|
||||
- {id: 55, cat: Audio/Lossless, desc: "Flac"}
|
||||
# Movies
|
||||
- {id: 17, cat: Movies/SD, desc: "Cine News"}
|
||||
- {id: 23, cat: Movies/SD, desc: "BDRip"}
|
||||
- {id: 43, cat: Movies/SD, desc: "BDRip"}
|
||||
- {id: 16, cat: Movies/SD, desc: "DivX"}
|
||||
- {id: 32, cat: Movies/SD, desc: "DVDRip"}
|
||||
- {id: 11, cat: Movies/DVD, desc: "DVD"}
|
||||
- {id: 29, cat: Movies/HD, desc: "720p"}
|
||||
- {id: 30, cat: Movies/HD, desc: "1080p"}
|
||||
- {id: 35, cat: Movies/BluRay, desc: "Blu Ray Disk"}
|
||||
- {id: 40, cat: Movies/HD, desc: "H-265"}
|
||||
- {id: 56, cat: Movies/3D, desc: "FullHD-3D"}
|
||||
- {id: 27, cat: TV/SD, desc: "SerieTV"}
|
||||
- {id: 20, cat: Movies/SD, desc: "DVDRip"}
|
||||
- {id: 21, cat: Movies/DVD, desc: "DVD"}
|
||||
- {id: 25, cat: Movies/HD, desc: "720p"}
|
||||
- {id: 24, cat: Movies/HD, desc: "1080p"}
|
||||
- {id: 27, cat: Movies/BluRay, desc: "Blu Ray Disk"}
|
||||
- {id: 23, cat: Movies/HD, desc: "H-265"}
|
||||
- {id: 26, cat: Movies/3D, desc: "3D-FullHD"}
|
||||
- {id: 31, cat: TV/SD, desc: "SerieTV"}
|
||||
- {id: 45, cat: TV/HD, desc: "Serie Tv HD"}
|
||||
- {id: 44, cat: Movies/UHD, desc: "4K Ultra HD"}
|
||||
- {id: 22, cat: Movies/UHD, desc: "4K-Ultra-HD"}
|
||||
- {id: 49, cat: TV/Documentary, desc: "Documentari"}
|
||||
- {id: 50, cat: TV/Other, desc: "Programmi TV"}
|
||||
- {id: 51, cat: Movies/Other, desc: "Mp4"}
|
||||
|
||||
- {id: 5, cat: TV/Anime, desc: "Anime"}
|
||||
- {id: 31, cat: TV/Anime, desc: "Cartoni Animati"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
|
@@ -34,15 +34,8 @@
|
||||
page: 1
|
||||
srcrel: "{{ .Keywords }}"
|
||||
keywordsfilters:
|
||||
- name: re_replace
|
||||
args: ["S[0-9]{2}([^E]|$)", ""] # remove season tag without episode (search doesn't support it)
|
||||
- name: diacritics
|
||||
args: replace
|
||||
# most ITA TV torrents are in XXxYY format, so we search without S/E prefixes and filter later
|
||||
- name: re_replace
|
||||
args: ["S0?(\\d{1,2})", " $1 "]
|
||||
- name: re_replace
|
||||
args: ["E(\\d{2,3})", " $1 "]
|
||||
rows:
|
||||
selector: div.showrelease_tb table tbody tr:not(tr:nth-child(1))
|
||||
fields:
|
||||
@@ -54,27 +47,9 @@
|
||||
filters:
|
||||
- name: split
|
||||
args: ["=", "-1"]
|
||||
# inizio prova
|
||||
- name: re_replace # replace special characters with " " (space)
|
||||
args: ["[^a-zA-Z0-9]|\\.", " "]
|
||||
# normalize to SXXEYY format
|
||||
- name: re_replace
|
||||
args: ["(\\d{2})x(\\d{2})", "S$1E$2"]
|
||||
- name: re_replace
|
||||
args: ["(\\d{1})x(\\d{2})", "S0$1E$2"]
|
||||
- name: re_replace #Stagione X --> S0X
|
||||
args: ["Stagione (\\d{0,1}\\s)", "S0$1"]
|
||||
- name: re_replace #Stagione XX --> SXX
|
||||
args: ["Stagione (\\d{2}\\s)", "S$1"]
|
||||
- name: re_replace #/ Episodio [YY-YY --> EYY-YY
|
||||
args: ["(\\s\\/\\sEpisodio|\\s\\/\\sEpisodi|\\sEpisodio|\\s\\|\\sEpisodio|\\sEpisodi)\\s\\[", "E"]
|
||||
- name: re_replace #/ Completa [episodi YY-YY --> EYY-YY
|
||||
args: ["(\\s\\/\\sCompleta\\s\\[episodi\\s)", "E"]
|
||||
- name: re_replace #remove di YY] | remove /YY]
|
||||
args: ["(\\sdi\\s\\d{1,2}|\\/\\d{1,2})\\]", " "]
|
||||
- name: re_replace #remove various
|
||||
args: ["(Serie completa|Completa|\\[in pausa\\])", ""]
|
||||
# fine prova
|
||||
args: ["\\b([s])?(\\d{1,3})[x\\s](\\d{1,3})", "S$2E$3"]
|
||||
details:
|
||||
selector: td:nth-child(7) a
|
||||
attribute: href
|
||||
|
@@ -6,8 +6,9 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://www.torrent9.ec/
|
||||
- http://www.torrent9.blue/
|
||||
legacylinks:
|
||||
- http://www.torrent9.ec/
|
||||
- http://www.torrent9.red/
|
||||
- http://www.torrent9.bz/
|
||||
- http://www.torrents9.pe/
|
||||
|
@@ -78,6 +78,9 @@
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}cats[]={{.}}&{{end}}"
|
||||
search: "{{ .Keywords }}"
|
||||
keywordsfilters:
|
||||
- name: replace
|
||||
args: [".", " "] # issue #3296
|
||||
rows:
|
||||
selector: table> tbody > tr[class^="torrents_table_row_"]
|
||||
filters:
|
||||
|
@@ -72,8 +72,8 @@
|
||||
tv-search: [q, season, ep]
|
||||
|
||||
login:
|
||||
path: /takelogin.php
|
||||
method: post
|
||||
path: /login.php
|
||||
method: form
|
||||
form: form
|
||||
inputs:
|
||||
username: "{{ .Config.username }}"
|
||||
|
@@ -70,6 +70,22 @@
|
||||
args: ["20[0-2][0-9] [0-9][0-9]", ""]
|
||||
- name: re_replace
|
||||
args: ["20[0-2][0-9]", ""]
|
||||
- name: replace
|
||||
args: ["ESP", "Spanish"]
|
||||
- name: re_replace
|
||||
args: ["[EI]NG", "English"]
|
||||
- name: replace
|
||||
args: ["CAT", "Catalan"]
|
||||
- name: replace
|
||||
args: ["FRA", "French"]
|
||||
- name: replace
|
||||
args: ["JAP", "Japanese"]
|
||||
- name: replace
|
||||
args: ["ITA", "Italian"]
|
||||
- name: replace
|
||||
args: ["RUS", "Russian"]
|
||||
- name: replace
|
||||
args: ["DUAL", "Spanish English"]
|
||||
details:
|
||||
selector: td[valign="middle"] a
|
||||
attribute: href
|
||||
@@ -98,8 +114,8 @@
|
||||
attribute: href
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
img[src$="gold.gif"]: "0"
|
||||
img[src$="silver.gif"]: "0.5"
|
||||
img[src$="gold.png"]: "0"
|
||||
img[src$="silver.png"]: "0.5"
|
||||
"*": "1"
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
|
@@ -244,6 +244,11 @@
|
||||
args: " ago"
|
||||
size:
|
||||
selector: "td:nth-child(6)"
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["o", "B"]
|
||||
grabs:
|
||||
selector: "td:nth-child(7)"
|
||||
seeders:
|
||||
text: 0
|
||||
seeders:
|
||||
|
@@ -33,6 +33,8 @@
|
||||
- path: "/search?{{if .Keywords}}s=ns&v=t&sd=d&q={{ .Keywords}}{{else}}s=dt&v=t&sd=d&q= *{{end}}{{if .Categories}} category:{{range .Categories}}{{.}},{{end}}{{else}}{{end}}"
|
||||
rows:
|
||||
selector: tr:has(td[class^="text-muted3"])
|
||||
filters:
|
||||
- name: andmatch
|
||||
fields:
|
||||
title:
|
||||
selector: td:nth-child(2) a
|
||||
|
@@ -13,7 +13,6 @@ using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.LayoutRenderers;
|
||||
using NLog.Targets;
|
||||
|
||||
namespace Jackett.Common
|
||||
@@ -179,7 +178,7 @@ namespace Jackett.Common
|
||||
var logFileName = settings.CustomLogFileName ?? "log.txt";
|
||||
var logLevel = settings.TracingEnabled ? LogLevel.Debug : LogLevel.Info;
|
||||
// Add custom date time format renderer as the default is too long
|
||||
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("simpledatetime", typeof(SimpleDateTimeRenderer));
|
||||
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("simpledatetime", typeof(Utils.LoggingSetup.SimpleDateTimeRenderer));
|
||||
|
||||
var logConfig = new LoggingConfiguration();
|
||||
var logFile = new FileTarget();
|
||||
@@ -265,13 +264,4 @@ namespace Jackett.Common
|
||||
ConfigService.SaveConfig(ServerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
[LayoutRenderer("simpledatetime")]
|
||||
public class SimpleDateTimeRenderer : LayoutRenderer
|
||||
{
|
||||
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
|
||||
{
|
||||
builder.Append(DateTime.Now.ToString("MM-dd HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
}
|
@@ -239,10 +239,40 @@ namespace Jackett.Common.Indexers.Abstract
|
||||
if (torrent["hasCue"] != null && (bool)torrent["hasCue"])
|
||||
flags.Add("Cue");
|
||||
|
||||
// tehconnection.me specific?
|
||||
var lang = (string)torrent["lang"];
|
||||
if (!string.IsNullOrEmpty(lang) && lang != "---")
|
||||
flags.Add(lang);
|
||||
|
||||
var media = (string)torrent["media"];
|
||||
if (!string.IsNullOrEmpty(media))
|
||||
flags.Add(media);
|
||||
|
||||
// tehconnection.me specific?
|
||||
var resolution = (string)torrent["resolution"];
|
||||
if (!string.IsNullOrEmpty(resolution))
|
||||
flags.Add(resolution);
|
||||
|
||||
// tehconnection.me specific?
|
||||
var container = (string)torrent["container"];
|
||||
if (!string.IsNullOrEmpty(container))
|
||||
flags.Add(container);
|
||||
|
||||
// tehconnection.me specific?
|
||||
var codec = (string)torrent["codec"];
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
flags.Add(codec);
|
||||
|
||||
// tehconnection.me specific?
|
||||
var audio = (string)torrent["audio"];
|
||||
if (!string.IsNullOrEmpty(audio))
|
||||
flags.Add(audio);
|
||||
|
||||
// tehconnection.me specific?
|
||||
var subbing = (string)torrent["subbing"];
|
||||
if (!string.IsNullOrEmpty(subbing) && subbing != "---")
|
||||
flags.Add(subbing);
|
||||
|
||||
if (torrent["remastered"] != null && (bool)torrent["remastered"])
|
||||
{
|
||||
var remasterYear = (string)torrent["remasterYear"];
|
||||
|
@@ -54,28 +54,28 @@ namespace Jackett.Common.Indexers
|
||||
Language = "pt-br";
|
||||
Type = "private";
|
||||
|
||||
AddCategoryMapping(14, TorznabCatType.TVAnime); // Anime
|
||||
AddCategoryMapping(3, TorznabCatType.PC0day); // Aplicativos
|
||||
AddCategoryMapping(8, TorznabCatType.Other); // Apostilas/Tutoriais
|
||||
AddCategoryMapping(19, TorznabCatType.AudioAudiobook); // Audiobook
|
||||
AddCategoryMapping(16, TorznabCatType.TVOTHER); // Desenho Animado
|
||||
AddCategoryMapping(18, TorznabCatType.TVDocumentary); // Documentários
|
||||
AddCategoryMapping(10, TorznabCatType.Books); // E-Books
|
||||
AddCategoryMapping(20, TorznabCatType.TVSport); // Esportes
|
||||
AddCategoryMapping(1, TorznabCatType.Movies); // Filmes
|
||||
AddCategoryMapping(12, TorznabCatType.MoviesOther); // Histórias em Quadrinhos
|
||||
AddCategoryMapping(5, TorznabCatType.Audio); // Músicas
|
||||
AddCategoryMapping(7, TorznabCatType.Other); // Outros
|
||||
AddCategoryMapping(9, TorznabCatType.BooksMagazines); // Revistas
|
||||
AddCategoryMapping(2, TorznabCatType.TV); // Seriados
|
||||
AddCategoryMapping(17, TorznabCatType.TV); // Shows
|
||||
AddCategoryMapping(13, TorznabCatType.TV); // Stand Up Comedy
|
||||
AddCategoryMapping(11, TorznabCatType.Other); // Video-Aula
|
||||
AddCategoryMapping(6, TorznabCatType.TV); // Vídeos de TV
|
||||
AddCategoryMapping(4, TorznabCatType.Other); // Jogos
|
||||
AddCategoryMapping(199, TorznabCatType.XXX); // Filmes Adultos
|
||||
AddCategoryMapping(200, TorznabCatType.XXX); // Jogos Adultos
|
||||
AddCategoryMapping(201, TorznabCatType.XXXImageset); // Fotos Adultas
|
||||
AddCategoryMapping(14, TorznabCatType.TVAnime, "Anime");
|
||||
AddCategoryMapping(3, TorznabCatType.PC0day, "Aplicativos");
|
||||
AddCategoryMapping(8, TorznabCatType.Other, "Apostilas/Tutoriais");
|
||||
AddCategoryMapping(19, TorznabCatType.AudioAudiobook, "Audiobook");
|
||||
AddCategoryMapping(16, TorznabCatType.TVOTHER, "Desenho Animado");
|
||||
AddCategoryMapping(18, TorznabCatType.TVDocumentary, "Documentários");
|
||||
AddCategoryMapping(10, TorznabCatType.Books, "E-Books");
|
||||
AddCategoryMapping(20, TorznabCatType.TVSport, "Esportes");
|
||||
AddCategoryMapping(1, TorznabCatType.Movies, "Filmes");
|
||||
AddCategoryMapping(12, TorznabCatType.MoviesOther, "Histórias em Quadrinhos");
|
||||
AddCategoryMapping(5, TorznabCatType.Audio, "Músicas");
|
||||
AddCategoryMapping(7, TorznabCatType.Other, "Outros");
|
||||
AddCategoryMapping(9, TorznabCatType.BooksMagazines, "Revistas");
|
||||
AddCategoryMapping(2, TorznabCatType.TV, "Seriados");
|
||||
AddCategoryMapping(17, TorznabCatType.TV, "Shows");
|
||||
AddCategoryMapping(13, TorznabCatType.TV, "Stand Up Comedy");
|
||||
AddCategoryMapping(11, TorznabCatType.Other, "Video-Aula");
|
||||
AddCategoryMapping(6, TorznabCatType.TV, "Vídeos de TV");
|
||||
AddCategoryMapping(4, TorznabCatType.Other, "Jogos");
|
||||
AddCategoryMapping(199, TorznabCatType.XXX, "Filmes Adultos");
|
||||
AddCategoryMapping(200, TorznabCatType.XXX, "Jogos Adultos");
|
||||
AddCategoryMapping(201, TorznabCatType.XXXImageset, "Fotos Adultas");
|
||||
}
|
||||
|
||||
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
||||
|
@@ -1,11 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using AutoMapper;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.IndexerConfig;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
@@ -14,6 +7,13 @@ using Jackett.Common.Utils.Clients;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
|
||||
|
||||
namespace Jackett.Common.Indexers
|
||||
{
|
||||
@@ -184,10 +184,10 @@ namespace Jackett.Common.Indexers
|
||||
return false;
|
||||
}
|
||||
|
||||
Version dotNetVersion = Microsoft.Extensions.PlatformAbstractions.PlatformServices.Default.Application.RuntimeFramework.Version;
|
||||
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("core", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
|
||||
if (!isWindows && dotNetVersion.Major < 4)
|
||||
if (!isWindows && runningOnDotNetCore)
|
||||
{
|
||||
// User isn't running Windows, but is running on .NET Core framework, no access to the DPAPI, so don't bother trying to migrate
|
||||
return false;
|
||||
@@ -195,17 +195,31 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
LoadValuesFromJson(jsonConfig, false);
|
||||
|
||||
object passwordPropertyValue = null;
|
||||
StringItem passwordPropertyValue = null;
|
||||
string passwordValue = "";
|
||||
|
||||
try
|
||||
{
|
||||
passwordPropertyValue = configData.GetType().GetProperty("Password").GetValue(configData, null);
|
||||
passwordValue = passwordPropertyValue.GetType().GetProperty("Value").GetValue(passwordPropertyValue, null).ToString();
|
||||
// try dynamic items first (e.g. all cardigann indexers)
|
||||
passwordPropertyValue = (StringItem)configData.GetDynamicByName("password");
|
||||
|
||||
if (passwordPropertyValue == null) // if there's no dynamic password try the static property
|
||||
{
|
||||
passwordPropertyValue = (StringItem)configData.GetType().GetProperty("Password").GetValue(configData, null);
|
||||
|
||||
// protection is based on the item.Name value (property name might be different, example: Abnormal), so check the Name again
|
||||
if (!string.Equals(passwordPropertyValue.Name, "password", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
logger.Debug($"Skipping non default password property (unencrpyted password) for [{ID}] while attempting migration");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
passwordValue = passwordPropertyValue.Value;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
logger.Debug($"Unable to source password for [{ID}] while attempting migration, likely a public tracker");
|
||||
logger.Debug($"Unable to source password for [{ID}] while attempting migration, likely a tracker without a password setting");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -231,7 +245,7 @@ namespace Jackett.Common.Indexers
|
||||
string unprotectedPassword = protectionService.LegacyUnProtect(passwordValue);
|
||||
//Password successfully unprotected using Windows/Mono DPAPI
|
||||
|
||||
passwordPropertyValue.GetType().GetProperty("Value").SetValue(passwordPropertyValue, unprotectedPassword);
|
||||
passwordPropertyValue.Value = unprotectedPassword;
|
||||
SaveConfig();
|
||||
IsConfigured = true;
|
||||
|
||||
@@ -290,7 +304,7 @@ namespace Jackett.Common.Indexers
|
||||
return false;
|
||||
if (caps.SupportsImdbSearch && query.IsImdbQuery)
|
||||
return true;
|
||||
else if(!caps.SupportsImdbSearch && query.IsImdbQuery && query.QueryType != "TorrentPotato") // potato query should always contain imdb+search term
|
||||
else if (!caps.SupportsImdbSearch && query.IsImdbQuery && query.QueryType != "TorrentPotato") // potato query should always contain imdb+search term
|
||||
return false;
|
||||
if (caps.SearchAvailable && query.IsSearch)
|
||||
return true;
|
||||
@@ -838,7 +852,6 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
public override TorznabCapabilities TorznabCaps { get; protected set; }
|
||||
|
||||
|
||||
private List<CategoryMapping> categoryMapping = new List<CategoryMapping>();
|
||||
protected WebClient webclient;
|
||||
protected readonly string downloadUrlBase = "";
|
||||
|
@@ -23,9 +23,9 @@ namespace Jackett.Common.Indexers
|
||||
private string LoginUrl { get { return SiteLink + "login.php"; } }
|
||||
private const int MAXPAGES = 3;
|
||||
|
||||
private new ConfigurationDataBasicLogin configData
|
||||
private new ConfigurationDataRecaptchaLogin configData
|
||||
{
|
||||
get { return (ConfigurationDataBasicLogin)base.configData; }
|
||||
get { return (ConfigurationDataRecaptchaLogin)base.configData; }
|
||||
set { base.configData = value; }
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Jackett.Common.Indexers
|
||||
client: w,
|
||||
logger: l,
|
||||
p: ps,
|
||||
configData: new ConfigurationDataBasicLogin())
|
||||
configData: new ConfigurationDataRecaptchaLogin())
|
||||
{
|
||||
Encoding = Encoding.GetEncoding("windows-1255");
|
||||
Language = "he-il";
|
||||
@@ -95,9 +95,57 @@ namespace Jackett.Common.Indexers
|
||||
AddCategoryMapping(76, TorznabCatType.TV, "סדרות");
|
||||
}
|
||||
|
||||
public override async Task<ConfigurationData> GetConfigurationForSetup()
|
||||
{
|
||||
var loginPage = await RequestStringWithCookies(LoginUrl, string.Empty);
|
||||
CQ cq = loginPage.Content;
|
||||
var captcha = cq.Find(".g-recaptcha"); // invisible recaptcha
|
||||
if (captcha.Any())
|
||||
{
|
||||
var result = this.configData;
|
||||
result.CookieHeader.Value = loginPage.Cookies;
|
||||
result.Captcha.SiteKey = captcha.Attr("data-sitekey");
|
||||
result.Captcha.Version = "2";
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new ConfigurationDataBasicLogin();
|
||||
result.SiteLink.Value = configData.SiteLink.Value;
|
||||
result.Instructions.Value = configData.Instructions.Value;
|
||||
result.Username.Value = configData.Username.Value;
|
||||
result.Password.Value = configData.Password.Value;
|
||||
result.CookieHeader.Value = loginPage.Cookies;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
||||
{
|
||||
LoadValuesFromJson(configJson);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(configData.Captcha.Cookie))
|
||||
{
|
||||
CookieHeader = configData.Captcha.Cookie;
|
||||
try
|
||||
{
|
||||
var results = await PerformQuery(new TorznabQuery());
|
||||
if (results.Count() == 0)
|
||||
{
|
||||
throw new Exception("Your cookie did not work");
|
||||
}
|
||||
|
||||
IsConfigured = true;
|
||||
SaveConfig();
|
||||
return IndexerConfigurationStatus.Completed;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
IsConfigured = false;
|
||||
throw new Exception("Your cookie did not work: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
var loginPage = await RequestStringWithCookies(LoginUrl, string.Empty);
|
||||
|
||||
var pairs = new Dictionary<string, string> {
|
||||
|
@@ -389,7 +389,15 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
var dateString = document.QuerySelector("div.title-block > div.details-pane > div.left-box").TextContent;
|
||||
dateString = TrimString(dateString, "eng: ", " г."); // '... Дата выхода eng: 09 марта 2012 г. ...' -> '09 марта 2012'
|
||||
var date = DateTime.Parse(dateString, new CultureInfo(Language)); // dd mmmm yyyy
|
||||
DateTime date;
|
||||
if (dateString.Length == 4) //dateString might be just a year, e.g. https://www.lostfilm.tv/series/Ghosted/season_1/episode_14/
|
||||
{
|
||||
date = DateTime.ParseExact(dateString, "yyyy", CultureInfo.InvariantCulture).ToLocalTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
date = DateTime.Parse(dateString, new CultureInfo(Language)); // dd mmmm yyyy
|
||||
}
|
||||
|
||||
var urlDetails = new TrackerUrlDetails(playButton);
|
||||
var episodeReleases = await FetchTrackerReleases(urlDetails);
|
||||
|
976
src/Jackett.Common/Indexers/MejorTorrent.cs
Normal file
976
src/Jackett.Common/Indexers/MejorTorrent.cs
Normal file
@@ -0,0 +1,976 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom.Html;
|
||||
using AngleSharp.Parser.Html;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.IndexerConfig;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using Jackett.Common.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace Jackett.Common.Indexers
|
||||
{
|
||||
class MejorTorrent : BaseWebIndexer
|
||||
{
|
||||
public static Uri WebUri = new Uri("http://www.mejortorrent.com/");
|
||||
public static Uri DownloadUri = new Uri(WebUri, "secciones.php?sec=descargas&ap=contar_varios");
|
||||
private static Uri SearchUriBase = new Uri(WebUri, "secciones.php");
|
||||
public static Uri NewTorrentsUri = new Uri(WebUri, "secciones.php?sec=ultimos_torrents");
|
||||
public static Encoding MEEncoding = Encoding.GetEncoding("windows-1252");
|
||||
|
||||
public MejorTorrent(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps)
|
||||
: base(name: "MejorTorrent",
|
||||
description: "MejorTorrent - Hay veces que un torrent viene mejor! :)",
|
||||
link: WebUri.AbsoluteUri,
|
||||
caps: new TorznabCapabilities(TorznabCatType.TV,
|
||||
TorznabCatType.TVSD,
|
||||
TorznabCatType.TVHD,
|
||||
TorznabCatType.Movies),
|
||||
configService: configService,
|
||||
client: wc,
|
||||
logger: l,
|
||||
p: ps,
|
||||
configData: new ConfigurationData())
|
||||
{
|
||||
Encoding = MEEncoding;
|
||||
Language = "es-es";
|
||||
Type = "public";
|
||||
}
|
||||
|
||||
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
||||
{
|
||||
configData.LoadValuesFromJson(configJson);
|
||||
|
||||
WebUri = new Uri(configData.SiteLink.Value);
|
||||
DownloadUri = new Uri(WebUri, "secciones.php?sec=descargas&ap=contar_varios");
|
||||
SearchUriBase = new Uri(WebUri, "secciones.php");
|
||||
NewTorrentsUri = new Uri(WebUri, "secciones.php?sec=ultimos_torrents");
|
||||
|
||||
var releases = await PerformQuery(new TorznabQuery());
|
||||
|
||||
await ConfigureIfOK(string.Empty, releases.Count() > 0, () =>
|
||||
{
|
||||
throw new Exception("Could not find releases from this URL");
|
||||
});
|
||||
|
||||
return IndexerConfigurationStatus.Completed;
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
return await PerformQuery(query, 0);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query, int attempts)
|
||||
{
|
||||
var originalSearchTerm = query.SearchTerm;
|
||||
if (query.SearchTerm == null)
|
||||
{
|
||||
query.SearchTerm = "";
|
||||
}
|
||||
query.SearchTerm = query.SearchTerm.Replace("'", "");
|
||||
|
||||
var requester = new MejorTorrentRequester(this);
|
||||
var tvShowScraper = new TvShowScraper();
|
||||
var seasonScraper = new SeasonScraper();
|
||||
var downloadScraper = new DownloadScraper();
|
||||
var rssScraper = new RssScraper();
|
||||
var downloadGenerator = new DownloadGenerator(requester, downloadScraper);
|
||||
var tvShowPerformer = new TvShowPerformer(requester, tvShowScraper, seasonScraper, downloadGenerator);
|
||||
var rssPerformer = new RssPerformer(requester, rssScraper, seasonScraper, downloadGenerator);
|
||||
var movieSearchScraper = new MovieSearchScraper();
|
||||
var movieInfoScraper = new MovieInfoScraper();
|
||||
var movieDownloadScraper = new MovieDownloadScraper();
|
||||
var moviePerformer = new MoviePerformer(requester, movieSearchScraper, movieInfoScraper, movieDownloadScraper);
|
||||
|
||||
var releases = new List<ReleaseInfo>();
|
||||
|
||||
if (string.IsNullOrEmpty(query.SanitizedSearchTerm))
|
||||
{
|
||||
releases = (await rssPerformer.PerformQuery(query)).ToList();
|
||||
var movie = releases.First();
|
||||
movie.Category.Add(TorznabCatType.Movies.ID);
|
||||
releases.ToList().Add(movie);
|
||||
if (releases.Count() == 0)
|
||||
{
|
||||
releases = (await AliveCheck(tvShowPerformer)).ToList();
|
||||
}
|
||||
return releases;
|
||||
}
|
||||
|
||||
if (query.Categories.Contains(TorznabCatType.Movies.ID) || query.Categories.Count() == 0)
|
||||
{
|
||||
releases.AddRange(await moviePerformer.PerformQuery(query));
|
||||
}
|
||||
if (query.Categories.Contains(TorznabCatType.TV.ID) ||
|
||||
query.Categories.Contains(TorznabCatType.TVSD.ID) ||
|
||||
query.Categories.Contains(TorznabCatType.TVHD.ID) ||
|
||||
query.Categories.Count() == 0)
|
||||
{
|
||||
releases.AddRange(await tvShowPerformer.PerformQuery(query));
|
||||
}
|
||||
|
||||
query.SearchTerm = originalSearchTerm;
|
||||
return releases;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ReleaseInfo>> AliveCheck(TvShowPerformer tvShowPerformer)
|
||||
{
|
||||
IEnumerable<ReleaseInfo> releases = new List<ReleaseInfo>();
|
||||
var tests = new Queue<string>(new[] { "stranger things", "westworld", "friends" });
|
||||
while (releases.Count() == 0 && tests.Count > 0)
|
||||
{
|
||||
var query = new TorznabQuery();
|
||||
query.SearchTerm = tests.Dequeue();
|
||||
releases = await tvShowPerformer.PerformQuery(query);
|
||||
}
|
||||
return releases;
|
||||
}
|
||||
|
||||
public static Uri CreateSearchUri(string search)
|
||||
{
|
||||
var finalUri = SearchUriBase.AbsoluteUri;
|
||||
finalUri += "?sec=buscador&valor=" + WebUtilityHelpers.UrlEncode(search, MEEncoding);
|
||||
return new Uri(finalUri);
|
||||
}
|
||||
|
||||
interface IScraper<T>
|
||||
{
|
||||
T Extract(IHtmlDocument html);
|
||||
}
|
||||
|
||||
class RssScraper : IScraper<IEnumerable<KeyValuePair<MTReleaseInfo, Uri>>>
|
||||
{
|
||||
private readonly string LinkQuerySelector = "a[href*=\"/serie\"]";
|
||||
|
||||
public IEnumerable<KeyValuePair<MTReleaseInfo, Uri>> Extract(IHtmlDocument html)
|
||||
{
|
||||
var episodes = GetNewEpisodesScratch(html);
|
||||
var links = GetLinks(html);
|
||||
var results = new List<KeyValuePair<MTReleaseInfo, Uri>>();
|
||||
for (var i = 0; i < episodes.Count(); i++)
|
||||
{
|
||||
results.Add(new KeyValuePair<MTReleaseInfo, Uri>(episodes.ElementAt(i), links.ElementAt(i)));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<MTReleaseInfo> GetNewEpisodesScratch(IHtmlDocument html)
|
||||
{
|
||||
var tvShowsElements = html.QuerySelectorAll(LinkQuerySelector);
|
||||
var seasonLinks = tvShowsElements.Select(e => e.Attributes["href"].Value);
|
||||
var dates = GetDates(html);
|
||||
var titles = GetTitles(html);
|
||||
var qualities = GetQualities(html);
|
||||
var seasonsFirstEpisodesAndLast = GetSeasonsFirstEpisodesAndLast(html);
|
||||
|
||||
var episodes = new List<MTReleaseInfo>();
|
||||
for(var i = 0; i < tvShowsElements.Count(); i++)
|
||||
{
|
||||
var e = new MTReleaseInfo();
|
||||
e.TitleOriginal = titles.ElementAt(i);
|
||||
e.PublishDate = dates.ElementAt(i);
|
||||
e.CategoryText = qualities.ElementAt(i);
|
||||
var sfeal = seasonsFirstEpisodesAndLast.ElementAt(i);
|
||||
e.Season = sfeal.Key;
|
||||
e.EpisodeNumber = sfeal.Value.Key;
|
||||
if (sfeal.Value.Value != null && sfeal.Value.Value > sfeal.Value.Key)
|
||||
{
|
||||
e.Files = sfeal.Value.Value - sfeal.Value.Key + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Files = 1;
|
||||
}
|
||||
episodes.Add(e);
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private List<Uri> GetLinks(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll(LinkQuerySelector)
|
||||
.Select(e => e.Attributes["href"].Value)
|
||||
.Select(relativeLink => new Uri(WebUri, relativeLink))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<DateTime> GetDates(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll(LinkQuerySelector)
|
||||
.Select(e => e.PreviousElementSibling.TextContent)
|
||||
.Select(dateString => dateString.Split('-'))
|
||||
.Select(parts => new int[] { Int32.Parse(parts[0]), Int32.Parse(parts[1]), Int32.Parse(parts[2]) })
|
||||
.Select(intParts => new DateTime(intParts[0], intParts[1], intParts[2]))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<string> GetTitles(IHtmlDocument html)
|
||||
{
|
||||
var texts = LinkTexts(html);
|
||||
var completeTitles = texts.Select(text => text.Substring(0, text.IndexOf('-') - 1));
|
||||
var regex = new Regex(@".+\((.+)\)");
|
||||
var finalTitles = completeTitles.Select(title =>
|
||||
{
|
||||
var match = regex.Match(title);
|
||||
if (!match.Success) return title;
|
||||
return match.Groups[1].Value;
|
||||
});
|
||||
return finalTitles.ToList();
|
||||
}
|
||||
|
||||
private List<string> GetQualities(IHtmlDocument html)
|
||||
{
|
||||
var texts = LinkTexts(html);
|
||||
var regex = new Regex(@".+\[(.*)\].+");
|
||||
var qualities = texts.Select(text =>
|
||||
{
|
||||
var match = regex.Match(text);
|
||||
if (!match.Success) return "HDTV";
|
||||
var quality = match.Groups[1].Value;
|
||||
switch(quality)
|
||||
{
|
||||
case "720p":
|
||||
return "HDTV-720p";
|
||||
case "1080p":
|
||||
return "HDTV-1080p";
|
||||
default:
|
||||
return "HDTV";
|
||||
}
|
||||
});
|
||||
return qualities.ToList();
|
||||
}
|
||||
|
||||
private List<KeyValuePair<int, KeyValuePair<int,int?>>> GetSeasonsFirstEpisodesAndLast(IHtmlDocument html)
|
||||
{
|
||||
var texts = LinkTexts(html);
|
||||
// SEASON | START EPISODE | [END EPISODE]
|
||||
var regex = new Regex(@"(\d{1,2})x(\d{1,2})(?:.*\d{1,2}x(\d{1,2})?)?", RegexOptions.IgnoreCase);
|
||||
var seasonsFirstEpisodesAndLast = texts.Select(text =>
|
||||
{
|
||||
var match = regex.Match(text);
|
||||
int season = 0;
|
||||
int episode = 0;
|
||||
int? finalEpisode = null;
|
||||
if (!match.Success) return new KeyValuePair<int, KeyValuePair<int, int?>>(season, new KeyValuePair<int, int?>(episode, finalEpisode));
|
||||
season = Int32.Parse(match.Groups[1].Value);
|
||||
episode = Int32.Parse(match.Groups[2].Value);
|
||||
if (match.Groups[3].Success)
|
||||
{
|
||||
finalEpisode = Int32.Parse(match.Groups[3].Value);
|
||||
}
|
||||
return new KeyValuePair<int, KeyValuePair<int, int?>>(season, new KeyValuePair<int, int?>(episode, finalEpisode));
|
||||
});
|
||||
return seasonsFirstEpisodesAndLast.ToList();
|
||||
}
|
||||
|
||||
private List<string> LinkTexts(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll(LinkQuerySelector)
|
||||
.Select(e => e.TextContent).ToList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TvShowScraper : IScraper<IEnumerable<Season>>
|
||||
{
|
||||
public IEnumerable<Season> Extract(IHtmlDocument html)
|
||||
{
|
||||
var tvSelector = "a[href*=\"/serie-\"]";
|
||||
var seasonsElements = html.QuerySelectorAll(tvSelector).Select(e => e.ParentElement);
|
||||
|
||||
var newTvShows = new List<Season>();
|
||||
|
||||
// EXAMPLES:
|
||||
// Stranger Things - 1ª Temporada (HDTV)
|
||||
// Stranger Things - 1ª Temporada [720p] (HDTV-720p)
|
||||
var regex = new Regex(@"(.+) - ([0-9]+).*\((.*)\)");
|
||||
foreach (var seasonElement in seasonsElements)
|
||||
{
|
||||
var link = seasonElement.QuerySelector("a[href*=\"/serie-\"]").Attributes["href"].Value;
|
||||
var info = seasonElement.TextContent; // Stranger Things - 1 ...
|
||||
var searchMatch = regex.Match(info);
|
||||
if (!searchMatch.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int seasonNumber;
|
||||
if (!Int32.TryParse(searchMatch.Groups[2].Value, out seasonNumber))
|
||||
{
|
||||
seasonNumber = 0;
|
||||
}
|
||||
var season = new Season
|
||||
{
|
||||
Title = searchMatch.Groups[1].Value,
|
||||
Number = seasonNumber,
|
||||
Type = searchMatch.Groups[3].Value,
|
||||
Link = new Uri(WebUri, link)
|
||||
};
|
||||
|
||||
// EXAMPLE: El cuento de la criada (Handmaids Tale)
|
||||
var originalTitleRegex = new Regex(@".+\((.+)\)");
|
||||
var originalTitleMath = originalTitleRegex.Match(season.Title);
|
||||
if (originalTitleMath.Success)
|
||||
{
|
||||
season.Title = originalTitleMath.Groups[1].Value;
|
||||
}
|
||||
newTvShows.Add(season);
|
||||
}
|
||||
return newTvShows;
|
||||
}
|
||||
}
|
||||
|
||||
class SeasonScraper : IScraper<IEnumerable<MTReleaseInfo>>
|
||||
{
|
||||
public IEnumerable<MTReleaseInfo> Extract(IHtmlDocument html)
|
||||
{
|
||||
var episodesLinksHtml = html.QuerySelectorAll("a[href*=\"/serie-episodio-descargar-torrent\"]");
|
||||
var episodesTexts = episodesLinksHtml.Select(l => l.TextContent).ToList();
|
||||
var episodesLinks = episodesLinksHtml.Select(e => e.Attributes["href"].Value).ToList();
|
||||
var dates = episodesLinksHtml
|
||||
.Select(e => e.ParentElement.ParentElement.QuerySelector("div").TextContent)
|
||||
.Select(stringDate => stringDate.Replace("Fecha: ", ""))
|
||||
.Select(stringDate => stringDate.Split('-'))
|
||||
.Select(stringParts => new int[]{ Int32.Parse(stringParts[0]), Int32.Parse(stringParts[1]), Int32.Parse(stringParts[2]) })
|
||||
.Select(intParts => new DateTime(intParts[0], intParts[1], intParts[2]));
|
||||
|
||||
var episodes = episodesLinks.Select(e => new MTReleaseInfo()).ToList();
|
||||
|
||||
for (var i = 0; i < episodes.Count(); i++)
|
||||
{
|
||||
GuessEpisodes(episodes.ElementAt(i), episodesTexts.ElementAt(i));
|
||||
ExtractLinkInfo(episodes.ElementAt(i), episodesLinks.ElementAt(i));
|
||||
episodes.ElementAt(i).PublishDate = dates.ElementAt(i);
|
||||
}
|
||||
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private void GuessEpisodes(MTReleaseInfo release, string episodeText)
|
||||
{
|
||||
var seasonEpisodeRegex = new Regex(@"(\d{1,2}).*?(\d{1,2})", RegexOptions.IgnoreCase);
|
||||
var matchSeasonEpisode = seasonEpisodeRegex.Match(episodeText);
|
||||
if (!matchSeasonEpisode.Success) return;
|
||||
release.Season = Int32.Parse(matchSeasonEpisode.Groups[1].Value);
|
||||
release.EpisodeNumber = Int32.Parse(matchSeasonEpisode.Groups[2].Value);
|
||||
|
||||
char[] textArray = episodeText.ToCharArray();
|
||||
Array.Reverse(textArray);
|
||||
var reversedText = new string(textArray);
|
||||
var finalEpisodeRegex = new Regex(@"(\d{1,2})");
|
||||
var matchFinalEpisode = finalEpisodeRegex.Match(reversedText);
|
||||
if (!matchFinalEpisode.Success) return;
|
||||
var finalEpisodeArray = matchFinalEpisode.Groups[1].Value.ToCharArray();
|
||||
Array.Reverse(finalEpisodeArray);
|
||||
var finalEpisode = Int32.Parse(new string(finalEpisodeArray));
|
||||
if (finalEpisode > release.EpisodeNumber)
|
||||
{
|
||||
release.Files = (finalEpisode + 1) - release.EpisodeNumber;
|
||||
release.Size = release.Size * release.Files;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractLinkInfo(MTReleaseInfo release, String link)
|
||||
{
|
||||
// LINK FORMAT: /serie-episodio-descargar-torrent-${ID}-${TITLE}-${SEASON_NUMBER}x${EPISODE_NUMBER}[range].html
|
||||
var regex = new Regex(@"\/serie-episodio-descargar-torrent-(\d+)-(.*)-(\d{1,2}).*(\d{1,2}).*\.html", RegexOptions.IgnoreCase);
|
||||
var linkMatch = regex.Match(link);
|
||||
|
||||
if (!linkMatch.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
release.MejorTorrentID = linkMatch.Groups[1].Value;
|
||||
release.Title = linkMatch.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadScraper : IScraper<IEnumerable<Uri>>
|
||||
{
|
||||
public IEnumerable<Uri> Extract(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll("a[href*=\".torrent\"]")
|
||||
.Select(e => e.Attributes["href"].Value)
|
||||
.Select(link => new Uri(WebUri, link));
|
||||
}
|
||||
}
|
||||
|
||||
class Season
|
||||
{
|
||||
public String Title;
|
||||
public int Number;
|
||||
public Uri Link;
|
||||
public TorznabCategory Category; // HDTV or HDTV-720
|
||||
private string _type;
|
||||
public string Type
|
||||
{
|
||||
get { return _type; }
|
||||
set
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case "HDTV":
|
||||
Category = TorznabCatType.TVSD;
|
||||
_type = "SDTV";
|
||||
break;
|
||||
case "HDTV-720p":
|
||||
Category = TorznabCatType.TVHD;
|
||||
_type = "HDTV-720p";
|
||||
break;
|
||||
case "HDTV-1080p":
|
||||
Category = TorznabCatType.TVHD;
|
||||
_type = "HDTV-1080p";
|
||||
break;
|
||||
default:
|
||||
Category = TorznabCatType.TV;
|
||||
_type = "HDTV-720p";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MTReleaseInfo : ReleaseInfo
|
||||
{
|
||||
public string MejorTorrentID;
|
||||
public bool IsMovie;
|
||||
public int? Year;
|
||||
public int _season;
|
||||
public int _episodeNumber;
|
||||
private string _categoryText;
|
||||
private string _originalTitle;
|
||||
|
||||
public MTReleaseInfo()
|
||||
{
|
||||
this.Category = new List<int>();
|
||||
this.Grabs = 5;
|
||||
this.Files = 1;
|
||||
this.PublishDate = new DateTime();
|
||||
this.Peers = 1;
|
||||
this.Seeders = 1;
|
||||
this.Size = ReleaseInfo.BytesFromGB(1);
|
||||
this._originalTitle = "";
|
||||
}
|
||||
|
||||
public int Season { get { return _season; } set { _season = value; TitleOriginal = _originalTitle; } }
|
||||
|
||||
public int EpisodeNumber { get { return _episodeNumber; } set { _episodeNumber = value; TitleOriginal = _originalTitle; } }
|
||||
|
||||
public string CategoryText {
|
||||
get { return _categoryText; }
|
||||
set
|
||||
{
|
||||
if (IsMovie)
|
||||
{
|
||||
Category.Add(TorznabCatType.Movies.ID);
|
||||
_categoryText = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case "SDTV":
|
||||
Category.Add(TorznabCatType.TVSD.ID);
|
||||
_categoryText = "SDTV";
|
||||
break;
|
||||
case "HDTV":
|
||||
Category.Add(TorznabCatType.TVSD.ID);
|
||||
_categoryText = "SDTV";
|
||||
break;
|
||||
case "HDTV-720p":
|
||||
Category.Add(TorznabCatType.TVHD.ID);
|
||||
_categoryText = "HDTV-720p";
|
||||
break;
|
||||
case "HDTV-1080p":
|
||||
Category.Add(TorznabCatType.TVHD.ID);
|
||||
_categoryText = "HDTV-1080p";
|
||||
break;
|
||||
default:
|
||||
Category.Add(TorznabCatType.TV.ID);
|
||||
_categoryText = "HDTV-720p";
|
||||
break;
|
||||
}
|
||||
}
|
||||
TitleOriginal = _originalTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public int FinalEpisodeNumber { get { return (int)(EpisodeNumber + Files - 1); } }
|
||||
|
||||
public string TitleOriginal
|
||||
{
|
||||
get { return _originalTitle; }
|
||||
set
|
||||
{
|
||||
_originalTitle = value;
|
||||
if (_originalTitle != "")
|
||||
{
|
||||
Title = _originalTitle;
|
||||
Title = char.ToUpper(Title[0]) + Title.Substring(1);
|
||||
}
|
||||
var seasonAndEpisode = "";
|
||||
if (!Category.Contains(TorznabCatType.Movies.ID))
|
||||
{
|
||||
seasonAndEpisode = "S" + Season.ToString("00") + "E" + EpisodeNumber.ToString("00");
|
||||
if (Files > 1)
|
||||
{
|
||||
seasonAndEpisode += "-" + FinalEpisodeNumber.ToString("00");
|
||||
}
|
||||
}
|
||||
Title = String.Join(".", new List<string>() { Title, seasonAndEpisode, CategoryText, "Spanish" }.Where(s => s != ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MoviePerformer : IPerformer
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<Uri>> movieSearchScraper;
|
||||
private IScraper<MTReleaseInfo> movieInfoScraper;
|
||||
private IScraper<Uri> movieDownloadScraper;
|
||||
|
||||
public MoviePerformer(
|
||||
IRequester requester,
|
||||
IScraper<IEnumerable<Uri>> movieSearchScraper,
|
||||
IScraper<MTReleaseInfo> movieInfoScraper,
|
||||
IScraper<Uri> movieDownloadScraper)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.movieSearchScraper = movieSearchScraper;
|
||||
this.movieInfoScraper = movieInfoScraper;
|
||||
this.movieDownloadScraper = movieDownloadScraper;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
query = SanitizeQuery(query);
|
||||
var movies = await FetchMoviesBasedOnLongestWord(query);
|
||||
return movies;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<MTReleaseInfo>> FetchMoviesBasedOnLongestWord(TorznabQuery query)
|
||||
{
|
||||
var originalSearch = query.SearchTerm;
|
||||
var regexStr = ".*" + originalSearch.Replace(" ", ".*") + ".*";
|
||||
var regex = new Regex(regexStr, RegexOptions.IgnoreCase);
|
||||
query.SearchTerm = LongestWord(query);
|
||||
var movies = await FetchMovies(query);
|
||||
return movies.Where(m => regex.Match(m.Title).Success);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<MTReleaseInfo>> FetchMovies(TorznabQuery query)
|
||||
{
|
||||
var uriSearch = CreateSearchUri(query.SearchTerm);
|
||||
var htmlSearch = await requester.MakeRequest(uriSearch);
|
||||
var moviesInfoUris = movieSearchScraper.Extract(htmlSearch);
|
||||
var infoHtmlTasks = moviesInfoUris.Select(async u => await requester.MakeRequest(u));
|
||||
var infoHtmls = await Task.WhenAll(infoHtmlTasks);
|
||||
var movies = infoHtmls.Select(h => movieInfoScraper.Extract(h));
|
||||
|
||||
var tasks = movies.Select(async m =>
|
||||
{
|
||||
var html = await requester.MakeRequest(m.Link);
|
||||
return new KeyValuePair<MTReleaseInfo, IHtmlDocument>(m, html);
|
||||
});
|
||||
var moviesWithHtml = await Task.WhenAll(tasks.ToArray());
|
||||
movies = moviesWithHtml.Select(movieWithHtml =>
|
||||
{
|
||||
var movie = movieWithHtml.Key;
|
||||
var html = movieWithHtml.Value;
|
||||
movie.Link = movieDownloadScraper.Extract(html);
|
||||
movie.Guid = movieWithHtml.Key.Link;
|
||||
return movie;
|
||||
});
|
||||
|
||||
if (query.Year != null)
|
||||
{
|
||||
movies = movies
|
||||
.Where(m =>
|
||||
m.Year == query.Year ||
|
||||
m.Year == query.Year + 1 ||
|
||||
m.Year == query.Year - 1)
|
||||
.Select(m =>
|
||||
{
|
||||
m.TitleOriginal = m.TitleOriginal.Replace("(" + m.Year + ")", "(" + query.Year + ")");
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
return movies;
|
||||
}
|
||||
|
||||
private string LongestWord(TorznabQuery query)
|
||||
{
|
||||
var words = query.SearchTerm.Split(' ');
|
||||
if (words.Count() == 0) return null;
|
||||
var longestWord = words.First();
|
||||
foreach(var word in words)
|
||||
{
|
||||
if (word.Length >= longestWord.Length)
|
||||
{
|
||||
longestWord = word;
|
||||
}
|
||||
}
|
||||
return longestWord;
|
||||
}
|
||||
|
||||
private TorznabQuery SanitizeQuery(TorznabQuery query)
|
||||
{
|
||||
var regex = new Regex(@"\d{4}$");
|
||||
var match = regex.Match(query.SanitizedSearchTerm);
|
||||
if (match.Success)
|
||||
{
|
||||
var yearStr = match.Groups[0].Value;
|
||||
query.Year = Int32.Parse(yearStr);
|
||||
query.SearchTerm = query.SearchTerm.Replace(yearStr, "").Trim();
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
class MovieSearchScraper : IScraper<IEnumerable<Uri>>
|
||||
{
|
||||
public IEnumerable<Uri> Extract(IHtmlDocument html)
|
||||
{
|
||||
return html.QuerySelectorAll("a[href*=\"/peli-\"]")
|
||||
.Select(e => e.GetAttribute("href"))
|
||||
.Select(relativeUri => new Uri(WebUri, relativeUri));
|
||||
}
|
||||
}
|
||||
|
||||
class MovieInfoScraper : IScraper<MTReleaseInfo>
|
||||
{
|
||||
public MTReleaseInfo Extract(IHtmlDocument html)
|
||||
{
|
||||
var release = new MTReleaseInfo();
|
||||
release.IsMovie = true;
|
||||
var selectors = html.QuerySelectorAll("b");
|
||||
var titleSelector = html.QuerySelector("span>b");
|
||||
try
|
||||
{
|
||||
var title = titleSelector.TextContent;
|
||||
if (title.Contains("("))
|
||||
{
|
||||
title = title.Substring(0, title.IndexOf("(")).Trim();
|
||||
}
|
||||
release.TitleOriginal = title;
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
var year = selectors.Where(s => s.TextContent.ToLower().Contains("año"))
|
||||
.First().NextSibling.TextContent.Trim();
|
||||
release.Year = Int32.Parse(year);
|
||||
release.TitleOriginal += " (" + year + ")";
|
||||
} catch { }
|
||||
try
|
||||
{
|
||||
var dateStr = selectors.Where(s => s.TextContent.ToLower().Contains("fecha"))
|
||||
.First().NextSibling.TextContent.Trim();
|
||||
var date = Convert.ToDateTime(dateStr);
|
||||
release.PublishDate = date;
|
||||
} catch { }
|
||||
try
|
||||
{
|
||||
var sizeStr = selectors.Where(s => s.TextContent.ToLower().Contains("tamaño"))
|
||||
.First().NextSibling.TextContent.Trim();
|
||||
Regex rgx = new Regex(@"[^0-9,.]");
|
||||
long size;
|
||||
if (sizeStr.ToLower().Trim().EndsWith("mb"))
|
||||
{
|
||||
size = ReleaseInfo.BytesFromMB(float.Parse(rgx.Replace(sizeStr, "")));
|
||||
}
|
||||
else
|
||||
{
|
||||
sizeStr = rgx.Replace(sizeStr, "").Replace(",", ".");
|
||||
size = ReleaseInfo.BytesFromGB(float.Parse(rgx.Replace(sizeStr, "")));
|
||||
}
|
||||
release.Size = size;
|
||||
} catch { }
|
||||
try
|
||||
{
|
||||
var category = selectors.Where(s => s.TextContent.ToLower().Contains("formato"))
|
||||
.First().NextSibling.TextContent.Trim();
|
||||
release.CategoryText = category;
|
||||
} catch { }
|
||||
try
|
||||
{
|
||||
var title = titleSelector.TextContent;
|
||||
if (title.Contains("(") && title.Contains(")") && title.Contains("4k"))
|
||||
{
|
||||
release.CategoryText = "2160p";
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
var link = html.QuerySelector("a[href*=\"sec=descargas\"]").GetAttribute("href");
|
||||
release.Link = new Uri(WebUri, link);
|
||||
release.Guid = release.Link;
|
||||
}
|
||||
catch { }
|
||||
return release;
|
||||
}
|
||||
}
|
||||
|
||||
class MovieDownloadScraper : IScraper<Uri>
|
||||
{
|
||||
public Uri Extract(IHtmlDocument html)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Uri(WebUri, html.QuerySelector("a[href*=\".torrent\"]").GetAttribute("href"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IRequester
|
||||
{
|
||||
Task<IHtmlDocument> MakeRequest(
|
||||
Uri uri,
|
||||
RequestType method = RequestType.GET,
|
||||
IEnumerable<KeyValuePair<string, string>> data = null,
|
||||
Dictionary<string, string> headers = null);
|
||||
}
|
||||
|
||||
class MejorTorrentRequester : IRequester
|
||||
{
|
||||
private MejorTorrent mt;
|
||||
|
||||
public MejorTorrentRequester(MejorTorrent mt)
|
||||
{
|
||||
this.mt = mt;
|
||||
}
|
||||
|
||||
public async Task<IHtmlDocument> MakeRequest(
|
||||
Uri uri,
|
||||
RequestType method = RequestType.GET,
|
||||
IEnumerable<KeyValuePair<string, string>> data = null,
|
||||
Dictionary<string, string> headers = null)
|
||||
{
|
||||
var result = await mt.RequestBytesWithCookies(uri.AbsoluteUri, null, method, null, data, headers);
|
||||
var SearchResultParser = new HtmlParser();
|
||||
var doc = SearchResultParser.Parse(mt.Encoding.GetString(result.Content));
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
|
||||
class MejorTorrentDownloadRequesterDecorator
|
||||
{
|
||||
private IRequester r;
|
||||
|
||||
public MejorTorrentDownloadRequesterDecorator(IRequester r)
|
||||
{
|
||||
this.r = r;
|
||||
}
|
||||
|
||||
public async Task<IHtmlDocument> MakeRequest(IEnumerable<string> ids)
|
||||
{
|
||||
var downloadHtmlTasks = new List<Task<IHtmlDocument>>();
|
||||
var formData = new List<KeyValuePair<string, string>>();
|
||||
int index = 1;
|
||||
ids.ToList().ForEach(id =>
|
||||
{
|
||||
var episodeID = new KeyValuePair<string, string>("episodios[" + index + "]", id);
|
||||
formData.Add(episodeID);
|
||||
index++;
|
||||
});
|
||||
formData.Add(new KeyValuePair<string, string>("total_capis", index.ToString()));
|
||||
formData.Add(new KeyValuePair<string, string>("tabla", "series"));
|
||||
return await r.MakeRequest(DownloadUri, RequestType.POST, formData);
|
||||
}
|
||||
}
|
||||
|
||||
interface IPerformer
|
||||
{
|
||||
Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query);
|
||||
}
|
||||
|
||||
class RssPerformer : IPerformer
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<KeyValuePair<MTReleaseInfo, Uri>>> rssScraper;
|
||||
private IScraper<IEnumerable<MTReleaseInfo>> seasonScraper;
|
||||
private IDownloadGenerator downloadGenerator;
|
||||
|
||||
public RssPerformer(
|
||||
IRequester requester,
|
||||
IScraper<IEnumerable<KeyValuePair<MTReleaseInfo, Uri>>> rssScraper,
|
||||
IScraper<IEnumerable<MTReleaseInfo>> seasonScraper,
|
||||
IDownloadGenerator downloadGenerator)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.rssScraper = rssScraper;
|
||||
this.seasonScraper = seasonScraper;
|
||||
this.downloadGenerator = downloadGenerator;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
var html = await requester.MakeRequest(NewTorrentsUri);
|
||||
var episodesAndSeasonsUri = rssScraper.Extract(html);
|
||||
|
||||
Task.WaitAll(episodesAndSeasonsUri.ToList().Select(async epAndSeasonUri =>
|
||||
{
|
||||
var episode = epAndSeasonUri.Key;
|
||||
var seasonUri = epAndSeasonUri.Value;
|
||||
await AddMejorTorrentIDs(episode, seasonUri);
|
||||
}).ToArray());
|
||||
|
||||
var episodes = episodesAndSeasonsUri.Select(epAndSeason => epAndSeason.Key).ToList();
|
||||
await downloadGenerator.AddDownloadLinks(episodes);
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private async Task AddMejorTorrentIDs(MTReleaseInfo episode, Uri seasonUri)
|
||||
{
|
||||
var html = await requester.MakeRequest(seasonUri);
|
||||
var newEpisodes = seasonScraper.Extract(html);
|
||||
// GET BY EPISODE NUMBER
|
||||
newEpisodes = newEpisodes.Where(e => e.EpisodeNumber == episode.EpisodeNumber);
|
||||
if (newEpisodes.Count() == 0)
|
||||
{
|
||||
throw new Exception("Imposible to detect episode ID in RSS");
|
||||
}
|
||||
episode.MejorTorrentID = newEpisodes.First().MejorTorrentID;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TvShowPerformer : IPerformer
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<Season>> tvShowScraper;
|
||||
private IScraper<IEnumerable<MTReleaseInfo>> seasonScraper;
|
||||
private IDownloadGenerator downloadGenerator;
|
||||
|
||||
public TvShowPerformer(
|
||||
IRequester requester,
|
||||
IScraper<IEnumerable<Season>> tvShowScraper,
|
||||
IScraper<IEnumerable<MTReleaseInfo>> seasonScraper,
|
||||
IDownloadGenerator downloadGenerator)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.tvShowScraper = tvShowScraper;
|
||||
this.seasonScraper = seasonScraper;
|
||||
this.downloadGenerator = downloadGenerator;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
query = FixQuery(query);
|
||||
var seasons = await GetSeasons(query);
|
||||
var episodes = await GetEpisodes(query, seasons);
|
||||
await downloadGenerator.AddDownloadLinks(episodes);
|
||||
if (seasons.Count() > 0)
|
||||
{
|
||||
episodes.ForEach(e => e.TitleOriginal = seasons.First().Title);
|
||||
}
|
||||
return episodes;
|
||||
}
|
||||
|
||||
private TorznabQuery FixQuery(TorznabQuery query)
|
||||
{
|
||||
var seasonRegex = new Regex(@".*?(s\d{1,2})", RegexOptions.IgnoreCase);
|
||||
var episodeRegex = new Regex(@".*?(e\d{1,2})", RegexOptions.IgnoreCase);
|
||||
var seasonMatch = seasonRegex.Match(query.SearchTerm);
|
||||
var episodeMatch = episodeRegex.Match(query.SearchTerm);
|
||||
if (seasonMatch.Success)
|
||||
{
|
||||
query.Season = Int32.Parse(seasonMatch.Groups[1].Value.Substring(1));
|
||||
query.SearchTerm = query.SearchTerm.Replace(seasonMatch.Groups[1].Value, "");
|
||||
}
|
||||
if (episodeMatch.Success)
|
||||
{
|
||||
query.Episode = episodeMatch.Groups[1].Value.Substring(1);
|
||||
query.SearchTerm = query.SearchTerm.Replace(episodeMatch.Groups[1].Value, "");
|
||||
}
|
||||
query.SearchTerm = query.SearchTerm.Trim();
|
||||
return query;
|
||||
}
|
||||
|
||||
private async Task<List<Season>> GetSeasons(TorznabQuery query)
|
||||
{
|
||||
var seasonHtml = await requester.MakeRequest(CreateSearchUri(query.SanitizedSearchTerm));
|
||||
var seasons = tvShowScraper.Extract(seasonHtml);
|
||||
if (query.Season != 0)
|
||||
{
|
||||
seasons = seasons.Where(s => s.Number == query.Season);
|
||||
}
|
||||
if (query.Categories.Count() != 0)
|
||||
{
|
||||
seasons = seasons.Where(s => new List<int>(query.Categories).Contains(s.Category.ID));
|
||||
}
|
||||
return seasons.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<MTReleaseInfo>> GetEpisodes(TorznabQuery query, IEnumerable<Season> seasons)
|
||||
{
|
||||
var episodesHtmlTasks = new Dictionary<Season, Task<IHtmlDocument>>();
|
||||
seasons.ToList().ForEach(season =>
|
||||
{
|
||||
episodesHtmlTasks.Add(season, requester.MakeRequest(new Uri(WebUri, season.Link)));
|
||||
});
|
||||
var episodesHtml = await Task.WhenAll(episodesHtmlTasks.Values);
|
||||
var episodes = episodesHtmlTasks.SelectMany(seasonAndHtml =>
|
||||
{
|
||||
var season = seasonAndHtml.Key;
|
||||
var html = seasonAndHtml.Value.Result;
|
||||
var eps = seasonScraper.Extract(html);
|
||||
return eps.ToList().Select(e =>
|
||||
{
|
||||
e.CategoryText = season.Type;
|
||||
return e;
|
||||
});
|
||||
});
|
||||
if (!string.IsNullOrEmpty(query.Episode))
|
||||
{
|
||||
var episodeNumber = Int32.Parse(query.Episode);
|
||||
episodes = episodes.Where(e => e.EpisodeNumber <= episodeNumber && episodeNumber <= e.FinalEpisodeNumber);
|
||||
}
|
||||
return episodes.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
interface IDownloadGenerator
|
||||
{
|
||||
Task AddDownloadLinks(IEnumerable<MTReleaseInfo> episodes);
|
||||
}
|
||||
|
||||
class DownloadGenerator : IDownloadGenerator
|
||||
{
|
||||
private IRequester requester;
|
||||
private IScraper<IEnumerable<Uri>> downloadScraper;
|
||||
|
||||
public DownloadGenerator(IRequester requester, IScraper<IEnumerable<Uri>> downloadScraper)
|
||||
{
|
||||
this.requester = requester;
|
||||
this.downloadScraper = downloadScraper;
|
||||
}
|
||||
|
||||
public async Task AddDownloadLinks(IEnumerable<MTReleaseInfo> episodes)
|
||||
{
|
||||
var downloadRequester = new MejorTorrentDownloadRequesterDecorator(requester);
|
||||
var downloadHtml = await downloadRequester.MakeRequest(episodes.Select(e => e.MejorTorrentID));
|
||||
var downloads = downloadScraper.Extract(downloadHtml).ToList();
|
||||
|
||||
for (var i = 0; i < downloads.Count; i++)
|
||||
{
|
||||
var e = episodes.ElementAt(i);
|
||||
episodes.ElementAt(i).Link = downloads.ElementAt(i);
|
||||
episodes.ElementAt(i).Guid = downloads.ElementAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -180,7 +180,7 @@ namespace Jackett.Common.Indexers
|
||||
}
|
||||
|
||||
var response = await RequestStringWithCookiesAndRetry(urlSearch);
|
||||
if (response.Status == System.Net.HttpStatusCode.Forbidden)
|
||||
if (response.Status == System.Net.HttpStatusCode.Forbidden || CookieHeader.Contains("pass=deleted"))
|
||||
{
|
||||
// re-login
|
||||
await ApplyConfiguration(null);
|
||||
|
@@ -227,8 +227,7 @@ namespace Jackett.Common.Indexers
|
||||
description = release.Description.Split('[');
|
||||
description[1] = "[" + description[1];
|
||||
}
|
||||
else
|
||||
|
||||
|
||||
release.Title = (description[0].Trim() + "." + seasonep.Trim() + "." + releasedata.Trim('.')).Replace(' ', '.');
|
||||
|
||||
// if search is done for S0X than we dont want to put . between S0X and E0X
|
||||
|
@@ -180,7 +180,7 @@ namespace Jackett.Common.Indexers
|
||||
var qCatLink = qRow.Find("a[href^=browse.php?cat=]").First();
|
||||
var qSeeders = qRow.Find("td > table.testtable > tbody > tr > td > strong:eq(3)");
|
||||
var qLeechers = qRow.Find("td > table.testtable > tbody > tr > td > strong:eq(4)");
|
||||
var qDateStr = qRow.Find("td > table.testtable > tbody > tr > td:eq(6)");
|
||||
var qDateStr = qRow.Find("td > table.testtable > tbody > tr > td:eq(7)");
|
||||
var qSize = qRow.Find("td > table.testtable > tbody > tr > td > strong:eq(1)");
|
||||
var qDownloadLink = qRow.Find("a[href*=download]").First();
|
||||
|
||||
|
@@ -28,15 +28,17 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
class NewpctRelease : ReleaseInfo
|
||||
{
|
||||
public string SerieName;
|
||||
public int? Season;
|
||||
public int? Episode;
|
||||
public int? EpisodeTo;
|
||||
}
|
||||
|
||||
private static Uri SiteLinkUri = new Uri("http://www.tvsinpagar.com/");
|
||||
private ReleaseInfo _mostRecentRelease;
|
||||
private Regex _searchStringRegex = new Regex(@"(.+?)S0?(\d+)(E0?(\d+))?$", RegexOptions.IgnoreCase);
|
||||
private Regex _titleListRegex = new Regex(@"Serie(.+?)(Temporada(.+?)(\d+)(.+?))?Capitulos?(.+?)(\d+)((.+?)(\d+))?(.+?)-(.+?)Calidad(.*)", RegexOptions.IgnoreCase);
|
||||
private Regex _titleClassicRegex = new Regex(@"(\[[^\]]*\])?\[Cap\.(\d{1,2})(\d{2})(_(\d{1,2})(\d{2}))?\]", RegexOptions.IgnoreCase);
|
||||
private Regex _titleClassicRegex = new Regex(@"(\[[^\]]*\])?\[Cap\.(\d{1,2})(\d{2})([_-](\d{1,2})(\d{2}))?\]", RegexOptions.IgnoreCase);
|
||||
private Regex _titleClassicTvQualityRegex = new Regex(@"\[([^\]]*HDTV[^\]]*)", RegexOptions.IgnoreCase);
|
||||
|
||||
private int _maxDailyPages = 7;
|
||||
@@ -51,7 +53,7 @@ namespace Jackett.Common.Indexers
|
||||
public Newpct(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps)
|
||||
: base(name: "Newpct",
|
||||
description: "Newpct - descargar torrent peliculas, series",
|
||||
link: "http://www.tvsinpagar.com/",
|
||||
link: SiteLinkUri.AbsoluteUri,
|
||||
caps: new TorznabCapabilities(TorznabCatType.TV,
|
||||
TorznabCatType.TVSD,
|
||||
TorznabCatType.TVHD,
|
||||
@@ -74,6 +76,7 @@ namespace Jackett.Common.Indexers
|
||||
{
|
||||
configData.LoadValuesFromJson(configJson);
|
||||
var releases = await PerformQuery(new TorznabQuery());
|
||||
SiteLinkUri = new Uri(configData.SiteLink.Value);
|
||||
|
||||
await ConfigureIfOK(string.Empty, releases.Count() > 0, () =>
|
||||
{
|
||||
@@ -108,14 +111,13 @@ namespace Jackett.Common.Indexers
|
||||
var releases = new List<ReleaseInfo>();
|
||||
|
||||
bool rssMode = string.IsNullOrEmpty(query.SanitizedSearchTerm);
|
||||
Uri siteLinkUri = new Uri(configData.SiteLink.Value);
|
||||
|
||||
if (rssMode)
|
||||
{
|
||||
int pg = 1;
|
||||
while (pg <= _maxDailyPages)
|
||||
{
|
||||
Uri url = new Uri(siteLinkUri, string.Format(_dailyUrl, pg));
|
||||
Uri url = new Uri(SiteLinkUri, string.Format(_dailyUrl, pg));
|
||||
var results = await RequestStringWithCookies(url.AbsoluteUri);
|
||||
|
||||
var items = ParseDailyContent(results.Content);
|
||||
@@ -142,105 +144,137 @@ namespace Jackett.Common.Indexers
|
||||
query.Categories.Any(c => _allTvCategories.Contains(c));
|
||||
if (isTvSearch)
|
||||
{
|
||||
var newpctReleases = new List<ReleaseInfo>();
|
||||
|
||||
string seriesName = query.SanitizedSearchTerm;
|
||||
int? season = query.Season > 0 ? (int?)query.Season : null;
|
||||
int? episode = null;
|
||||
if (!string.IsNullOrWhiteSpace(query.Episode) && int.TryParse(query.Episode, out int episodeTemp))
|
||||
episode = episodeTemp;
|
||||
|
||||
//If query has no season/episode info, try to parse title
|
||||
if (season == null && episode == null)
|
||||
{
|
||||
Match searchMatch = _searchStringRegex.Match(query.SanitizedSearchTerm);
|
||||
if (searchMatch.Success)
|
||||
{
|
||||
seriesName = searchMatch.Groups[1].Value.Trim();
|
||||
season = int.Parse(searchMatch.Groups[2].Value);
|
||||
episode = searchMatch.Groups[4].Success ? (int?)int.Parse(searchMatch.Groups[4].Value) : null;
|
||||
}
|
||||
}
|
||||
|
||||
//Try to reuse cache
|
||||
bool cacheFound = false;
|
||||
lock (cache)
|
||||
{
|
||||
CleanCache();
|
||||
var cachedResult = cache.FirstOrDefault(i => i.Query == seriesName.ToLower());
|
||||
if (cachedResult != null && cachedResult.Results != null)
|
||||
{
|
||||
cacheFound = true;
|
||||
newpctReleases = cachedResult.Results.Where(r => (r as NewpctRelease) != null).ToList();
|
||||
if (!newpctReleases.Any() && cachedResult.Results.Any())
|
||||
cacheFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cacheFound)
|
||||
{
|
||||
IEnumerable<string> lettersUrl;
|
||||
if (!((BoolItem)configData.GetDynamic("IncludeVo")).Value)
|
||||
lettersUrl = _seriesLetterUrls;
|
||||
else
|
||||
lettersUrl = _seriesLetterUrls.Concat(_seriesVOLetterUrls);
|
||||
|
||||
string seriesLetter = !char.IsDigit(seriesName[0]) ? seriesName[0].ToString() : "0-9";
|
||||
//Search series url
|
||||
foreach (string urlFormat in lettersUrl)
|
||||
{
|
||||
Uri seriesListUrl = new Uri(siteLinkUri, string.Format(urlFormat, seriesLetter.ToLower()));
|
||||
var results = await RequestStringWithCookies(seriesListUrl.AbsoluteUri);
|
||||
|
||||
//Episodes list
|
||||
string seriesEpisodesUrl = ParseSeriesListContent(results.Content, seriesName);
|
||||
if (!string.IsNullOrEmpty(seriesEpisodesUrl))
|
||||
{
|
||||
int pg = 1;
|
||||
while (pg < _maxEpisodesListPages)
|
||||
{
|
||||
Uri episodesListUrl = new Uri(string.Format(_seriesUrl, seriesEpisodesUrl, pg));
|
||||
results = await RequestStringWithCookies(episodesListUrl.AbsoluteUri);
|
||||
|
||||
var items = ParseEpisodesListContent(results.Content);
|
||||
if (items == null || !items.Any())
|
||||
break;
|
||||
|
||||
newpctReleases.AddRange(items);
|
||||
|
||||
pg++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Cache ALL episodes
|
||||
lock (cache)
|
||||
{
|
||||
cache.Add(new CachedQueryResult(seriesName.ToLower(), newpctReleases));
|
||||
}
|
||||
}
|
||||
|
||||
//Filter only episodes needed
|
||||
releases.AddRange(newpctReleases.Where(r =>
|
||||
{
|
||||
NewpctRelease nr = r as NewpctRelease;
|
||||
return nr.Season.HasValue != season.HasValue || //Can't determine if same season
|
||||
nr.Season.HasValue && season.Value == nr.Season.Value && //Same season and ...
|
||||
(
|
||||
nr.Episode.HasValue != episode.HasValue || //Can't determine if same episode
|
||||
nr.Episode.HasValue &&
|
||||
(
|
||||
nr.Episode.Value == episode.Value || //Same episode
|
||||
nr.EpisodeTo.HasValue && episode.Value >= nr.Episode.Value && episode.Value <= nr.EpisodeTo.Value //Episode in interval
|
||||
)
|
||||
);
|
||||
}));
|
||||
return await TvSearch(query);
|
||||
}
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ReleaseInfo>> TvSearch(TorznabQuery query)
|
||||
{
|
||||
var newpctReleases = new List<ReleaseInfo>();
|
||||
|
||||
string seriesName = query.SanitizedSearchTerm;
|
||||
int? season = query.Season > 0 ? (int?)query.Season : null;
|
||||
int? episode = null;
|
||||
if (!string.IsNullOrWhiteSpace(query.Episode) && int.TryParse(query.Episode, out int episodeTemp))
|
||||
episode = episodeTemp;
|
||||
|
||||
//If query has no season/episode info, try to parse title
|
||||
if (season == null && episode == null)
|
||||
{
|
||||
Match searchMatch = _searchStringRegex.Match(query.SanitizedSearchTerm);
|
||||
if (searchMatch.Success)
|
||||
{
|
||||
seriesName = searchMatch.Groups[1].Value.Trim();
|
||||
season = int.Parse(searchMatch.Groups[2].Value);
|
||||
episode = searchMatch.Groups[4].Success ? (int?)int.Parse(searchMatch.Groups[4].Value) : null;
|
||||
}
|
||||
}
|
||||
|
||||
//Try to reuse cache
|
||||
bool cacheFound = false;
|
||||
lock (cache)
|
||||
{
|
||||
CleanCache();
|
||||
var cachedResult = cache.FirstOrDefault(i => i.Query == seriesName.ToLower());
|
||||
if (cachedResult != null && cachedResult.Results != null)
|
||||
{
|
||||
cacheFound = true;
|
||||
newpctReleases = cachedResult.Results.Where(r => (r as NewpctRelease) != null).ToList();
|
||||
if (!newpctReleases.Any() && cachedResult.Results.Any())
|
||||
cacheFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cacheFound)
|
||||
{
|
||||
//Search series url
|
||||
foreach (Uri seriesListUrl in SeriesListUris(seriesName))
|
||||
{
|
||||
newpctReleases.AddRange(await GetReleasesFromUri(seriesListUrl, seriesName));
|
||||
}
|
||||
|
||||
//Sonarr removes "the" from shows. If there is nothing try prepending "the"
|
||||
if (newpctReleases.Count == 0 && !(seriesName.ToLower().StartsWith("the")))
|
||||
{
|
||||
seriesName = "The " + seriesName;
|
||||
foreach (Uri seriesListUrl in SeriesListUris(seriesName))
|
||||
{
|
||||
newpctReleases.AddRange(await GetReleasesFromUri(seriesListUrl, seriesName));
|
||||
}
|
||||
}
|
||||
|
||||
//Cache ALL episodes
|
||||
lock (cache)
|
||||
{
|
||||
cache.Add(new CachedQueryResult(seriesName.ToLower(), newpctReleases));
|
||||
}
|
||||
}
|
||||
|
||||
//Filter only episodes needed
|
||||
return newpctReleases.Where(r =>
|
||||
{
|
||||
NewpctRelease nr = r as NewpctRelease;
|
||||
return nr.Season.HasValue != season.HasValue || //Can't determine if same season
|
||||
nr.Season.HasValue && season.Value == nr.Season.Value && //Same season and ...
|
||||
(
|
||||
nr.Episode.HasValue != episode.HasValue || //Can't determine if same episode
|
||||
nr.Episode.HasValue &&
|
||||
(
|
||||
nr.Episode.Value == episode.Value || //Same episode
|
||||
nr.EpisodeTo.HasValue && episode.Value >= nr.Episode.Value && episode.Value <= nr.EpisodeTo.Value //Episode in interval
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ReleaseInfo>> GetReleasesFromUri(Uri uri, string seriesName)
|
||||
{
|
||||
var newpctReleases = new List<ReleaseInfo>();
|
||||
var results = await RequestStringWithCookies(uri.AbsoluteUri);
|
||||
|
||||
//Episodes list
|
||||
string seriesEpisodesUrl = ParseSeriesListContent(results.Content, seriesName);
|
||||
if (!string.IsNullOrEmpty(seriesEpisodesUrl))
|
||||
{
|
||||
int pg = 1;
|
||||
while (pg < _maxEpisodesListPages)
|
||||
{
|
||||
Uri episodesListUrl = new Uri(string.Format(_seriesUrl, seriesEpisodesUrl, pg));
|
||||
results = await RequestStringWithCookies(episodesListUrl.AbsoluteUri);
|
||||
|
||||
var items = ParseEpisodesListContent(results.Content);
|
||||
if (items == null || !items.Any())
|
||||
break;
|
||||
|
||||
newpctReleases.AddRange(items);
|
||||
|
||||
pg++;
|
||||
}
|
||||
}
|
||||
return newpctReleases;
|
||||
}
|
||||
|
||||
private IEnumerable<Uri> SeriesListUris(string seriesName)
|
||||
{
|
||||
IEnumerable<string> lettersUrl;
|
||||
if (!((BoolItem)configData.GetDynamic("IncludeVo")).Value)
|
||||
{
|
||||
lettersUrl = _seriesLetterUrls;
|
||||
}
|
||||
else
|
||||
{
|
||||
lettersUrl = _seriesLetterUrls.Concat(_seriesVOLetterUrls);
|
||||
}
|
||||
string seriesLetter = !char.IsDigit(seriesName[0]) ? seriesName[0].ToString() : "0-9";
|
||||
return lettersUrl.Select(urlFormat =>
|
||||
{
|
||||
return new Uri(SiteLinkUri, string.Format(urlFormat, seriesLetter.ToLower()));
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<NewpctRelease> ParseDailyContent(string content)
|
||||
{
|
||||
var SearchResultParser = new HtmlParser();
|
||||
@@ -364,7 +398,7 @@ namespace Jackett.Common.Indexers
|
||||
Match match = _titleListRegex.Match(title);
|
||||
if (match.Success)
|
||||
{
|
||||
string name = match.Groups[1].Value.Trim(' ', '-');
|
||||
result.SerieName = match.Groups[1].Value.Trim(' ', '-');
|
||||
result.Season = int.Parse(match.Groups[4].Success ? match.Groups[4].Value.Trim() : "1");
|
||||
result.Episode = int.Parse(match.Groups[7].Value.Trim().PadLeft(2, '0'));
|
||||
result.EpisodeTo = match.Groups[10].Success ? (int?)int.Parse(match.Groups[10].Value.Trim()) : null;
|
||||
@@ -376,7 +410,7 @@ namespace Jackett.Common.Indexers
|
||||
string episodeToText = result.EpisodeTo.HasValue ? "_" + seasonText + result.EpisodeTo.ToString().PadLeft(2, '0') : "";
|
||||
|
||||
result.Title = string.Format("{0} - Temporada {1} [{2}][Cap.{3}{4}][{5}]",
|
||||
name, seasonText, quality, episodeText, episodeToText, audioQuality);
|
||||
result.SerieName, seasonText, quality, episodeText, episodeToText, audioQuality);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -408,11 +442,45 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
result.Size = size;
|
||||
result.Link = new Uri(detailsUrl);
|
||||
result.Guid = result.Link;
|
||||
result.PublishDate = publishDate;
|
||||
result.Seeders = 1;
|
||||
result.Peers = 1;
|
||||
|
||||
result.Title = FixedTitle(result, quality);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string FixedTitle(NewpctRelease release, string quality)
|
||||
{
|
||||
if (String.IsNullOrEmpty(release.SerieName))
|
||||
{
|
||||
release.SerieName = release.Title;
|
||||
if (release.Title.Contains("-"))
|
||||
{
|
||||
release.SerieName = release.Title.Substring(0, release.Title.IndexOf('-') - 1);
|
||||
}
|
||||
}
|
||||
if (String.IsNullOrEmpty(quality))
|
||||
{
|
||||
quality = "HDTV";
|
||||
}
|
||||
var seasonAndEpisode = "S" + release.Season.ToString().PadLeft(2, '0');
|
||||
seasonAndEpisode += "E" + release.Episode.ToString().PadLeft(2, '0');
|
||||
if (release.EpisodeTo != release.Episode && release.EpisodeTo != null && release.EpisodeTo != 0)
|
||||
{
|
||||
seasonAndEpisode += "-" + release.EpisodeTo.ToString().PadLeft(2, '0');
|
||||
}
|
||||
var titleParts = new List<string>();
|
||||
titleParts.Add(release.SerieName);
|
||||
titleParts.Add(seasonAndEpisode);
|
||||
titleParts.Add(quality.Replace("[", "").Replace("]", ""));
|
||||
if (release.Title.ToLower().Contains("esp") || release.Title.ToLower().Contains("cast"))
|
||||
{
|
||||
titleParts.Add("Spanish");
|
||||
}
|
||||
return String.Join(".", titleParts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
src/Jackett.Common/Indexers/TehConnectionMe.cs
Normal file
32
src/Jackett.Common/Indexers/TehConnectionMe.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using Jackett.Common.Indexers.Abstract;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using NLog;
|
||||
|
||||
namespace Jackett.Common.Indexers
|
||||
{
|
||||
public class TehConnectionMe : GazelleTracker
|
||||
{
|
||||
public TehConnectionMe(IIndexerConfigurationService configService, WebClient webClient, Logger logger, IProtectionService protectionService)
|
||||
: base(name: "TehConnection.me",
|
||||
desc: "A movies tracker",
|
||||
link: "https://tehconnection.me/",
|
||||
configService: configService,
|
||||
logger: logger,
|
||||
protectionService: protectionService,
|
||||
webClient: webClient,
|
||||
supportsFreeleechTokens: true
|
||||
)
|
||||
{
|
||||
Language = "en-us";
|
||||
Type = "private";
|
||||
|
||||
AddCategoryMapping(1, TorznabCatType.Movies, "Feature Film");
|
||||
AddCategoryMapping(2, TorznabCatType.Movies, "Short Film");
|
||||
AddCategoryMapping(3, TorznabCatType.Movies, "Miniseries");
|
||||
AddCategoryMapping(4, TorznabCatType.Movies, "Other");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net452</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;net452;net461</TargetFrameworks>
|
||||
<Version>0.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -115,17 +115,16 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="0.9.9.2" />
|
||||
<PackageReference Include="Autofac" Version="4.8.1" />
|
||||
<PackageReference Include="AutoMapper" Version="6.2.2" />
|
||||
<PackageReference Include="AutoMapper" Version="7.0.1" />
|
||||
<PackageReference Include="BencodeNET" Version="2.2.24" />
|
||||
<PackageReference Include="CloudFlareUtilities" Version="1.2.0" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.2.1" />
|
||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
|
||||
<PackageReference Include="MimeMapping" Version="1.0.1.12" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="NLog" Version="4.5.6" />
|
||||
<PackageReference Include="YamlDotNet" Version="4.3.2-pre0473" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -189,10 +188,12 @@
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net452'">
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
|
||||
<PackageReference Include="CsQuery" Version="1.3.5-beta5" />
|
||||
<PackageReference Include="SharpZipLib" Version="0.86.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="1.1.2" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
@@ -208,6 +209,9 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities">
|
||||
<Version>2.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController">
|
||||
<Version>4.5.0</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -27,6 +27,7 @@ namespace Jackett.Common.Models.Config
|
||||
public bool UpdatePrerelease { get; set; }
|
||||
public string BasePathOverride { get; set; }
|
||||
public string OmdbApiKey { get; set; }
|
||||
public string OmdbApiUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ignore as we don't really want to be saving settings specified in the command line.
|
||||
|
@@ -30,6 +30,8 @@ namespace Jackett.Common.Models.DTO
|
||||
[DataMember]
|
||||
public string omdbkey { get; set; }
|
||||
[DataMember]
|
||||
public string omdburl { get; set; }
|
||||
[DataMember]
|
||||
public string app_version { get; set; }
|
||||
|
||||
[DataMember]
|
||||
@@ -61,6 +63,7 @@ namespace Jackett.Common.Models.DTO
|
||||
logging = config.RuntimeSettings.TracingEnabled;
|
||||
basepathoverride = config.BasePathOverride;
|
||||
omdbkey = config.OmdbApiKey;
|
||||
omdburl = config.OmdbApiUrl;
|
||||
app_version = version;
|
||||
|
||||
proxy_type = config.ProxyType;
|
||||
|
@@ -116,7 +116,7 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||
case ItemType.HiddenData:
|
||||
case ItemType.DisplayInfo:
|
||||
var value = ((StringItem)item).Value;
|
||||
if (string.Equals(item.Name, "password", StringComparison.InvariantCultureIgnoreCase))
|
||||
if (string.Equals(item.Name, "password", StringComparison.InvariantCultureIgnoreCase)) // if we chagne this logic we've to change the MigratedFromDPAPI() logic too, #2114 is realted
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
value = string.Empty;
|
||||
@@ -190,6 +190,11 @@ namespace Jackett.Common.Models.IndexerConfig
|
||||
}
|
||||
}
|
||||
|
||||
public Item GetDynamicByName(string Name)
|
||||
{
|
||||
return dynamics.Values.Where(i => string.Equals(i.Name, Name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
|
||||
}
|
||||
|
||||
public class Item
|
||||
{
|
||||
public ItemType ItemType { get; set; }
|
||||
|
@@ -53,6 +53,9 @@ namespace Jackett.Common.Plumbing
|
||||
// Register the best web client for the platform or the override
|
||||
switch (_runtimeSettings.ClientOverride)
|
||||
{
|
||||
case "httpclientnetcore":
|
||||
// do nothing, registered by the netcore app
|
||||
break;
|
||||
case "httpclient":
|
||||
RegisterWebClient<HttpWebClient>(builder);
|
||||
break;
|
||||
|
@@ -17,10 +17,11 @@ namespace Jackett.Common.Services
|
||||
|
||||
public class OmdbResolver : IImdbResolver
|
||||
{
|
||||
public OmdbResolver(WebClient webClient, NonNull<string> omdbApiKey)
|
||||
public OmdbResolver(WebClient webClient, NonNull<string> omdbApiKey, string omdbApiUrl)
|
||||
{
|
||||
WebClient = webClient;
|
||||
apiKey = omdbApiKey;
|
||||
url = omdbApiUrl;
|
||||
}
|
||||
|
||||
public async Task<Movie> MovieForId(NonNull<string> id)
|
||||
@@ -30,7 +31,10 @@ namespace Jackett.Common.Services
|
||||
if (!imdbId.StartsWith("tt", StringComparison.Ordinal))
|
||||
imdbId = "tt" + imdbId;
|
||||
|
||||
var request = new WebRequest("http://omdbapi.com/?apikey=" + apiKey + "&i=" + imdbId);
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
url = "http://omdbapi.com";
|
||||
|
||||
var request = new WebRequest(url + "/?apikey=" + apiKey + "&i=" + imdbId);
|
||||
request.Encoding = Encoding.UTF8;
|
||||
var result = await WebClient.GetString(request);
|
||||
var movie = JsonConvert.DeserializeObject<Movie>(result.Content);
|
||||
@@ -40,5 +44,6 @@ namespace Jackett.Common.Services
|
||||
|
||||
private WebClient WebClient;
|
||||
private string apiKey;
|
||||
private string url;
|
||||
}
|
||||
}
|
||||
|
@@ -160,7 +160,7 @@ namespace Jackett.Common.Services
|
||||
IResultFilterProvider resultFilterProvider = null;
|
||||
if (!omdbApiKey.IsNullOrEmptyOrWhitespace())
|
||||
{
|
||||
var imdbResolver = new OmdbResolver(webClient, omdbApiKey.ToNonNull());
|
||||
var imdbResolver = new OmdbResolver(webClient, omdbApiKey.ToNonNull(), serverConfig.OmdbApiUrl);
|
||||
fallbackStrategyProvider = new ImdbFallbackStrategyProvider(imdbResolver);
|
||||
resultFilterProvider = new ImdbTitleResultFilterProvider(imdbResolver);
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ namespace Jackett.Common.Services
|
||||
|
||||
public class TrayLockService : ITrayLockService
|
||||
{
|
||||
private readonly string EVENT_HANDLE_NAME = "JACKETT.TRAY";
|
||||
private readonly string EVENT_HANDLE_NAME = @"Global\JACKETT.TRAY";
|
||||
|
||||
private EventWaitHandle GetEventHandle()
|
||||
{
|
||||
|
@@ -98,6 +98,12 @@ namespace Jackett.Common.Services
|
||||
return;
|
||||
}
|
||||
|
||||
bool trayIsRunning = false;
|
||||
if (isWindows)
|
||||
{
|
||||
trayIsRunning = Process.GetProcessesByName("JackettTray").Length > 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -135,7 +141,7 @@ namespace Jackett.Common.Services
|
||||
var installDir = Path.GetDirectoryName(ExePath());
|
||||
var updaterPath = Path.Combine(tempDir, "Jackett", "JackettUpdater.exe");
|
||||
if (updaterPath != null)
|
||||
StartUpdate(updaterPath, installDir, isWindows, serverConfig.RuntimeSettings.NoRestart);
|
||||
StartUpdate(updaterPath, installDir, isWindows, serverConfig.RuntimeSettings.NoRestart, trayIsRunning);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -212,7 +218,7 @@ namespace Jackett.Common.Services
|
||||
|
||||
private async Task<string> DownloadRelease(List<Asset> assets, bool isWindows, string version)
|
||||
{
|
||||
var targetAsset = assets.Where(a => isWindows ? a.Browser_download_url.ToLowerInvariant().EndsWith(".zip") : a.Browser_download_url.ToLowerInvariant().EndsWith(".gz")).FirstOrDefault();
|
||||
var targetAsset = assets.Where(a => isWindows ? a.Browser_download_url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) : a.Browser_download_url.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
|
||||
|
||||
if (targetAsset == null)
|
||||
{
|
||||
@@ -262,17 +268,29 @@ namespace Jackett.Common.Services
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
private void StartUpdate(string updaterExePath, string installLocation, bool isWindows, bool NoRestart)
|
||||
private void StartUpdate(string updaterExePath, string installLocation, bool isWindows, bool NoRestart, bool trayIsRunning)
|
||||
{
|
||||
string appType = "Console";
|
||||
//DI once off Owin
|
||||
IProcessService processService = new ProcessService(logger);
|
||||
IServiceConfigService windowsService = new WindowsServiceConfigService(processService, logger);
|
||||
|
||||
if (isWindows && windowsService.ServiceExists() && windowsService.ServiceRunning())
|
||||
{
|
||||
appType = "WindowsService";
|
||||
}
|
||||
|
||||
var exe = Path.GetFileName(ExePath());
|
||||
var args = string.Join(" ", Environment.GetCommandLineArgs().Skip(1).Select(a => a.Contains(" ") ? "\"" +a + "\"" : a )).Replace("\"", "\\\"");
|
||||
|
||||
var startInfo = new ProcessStartInfo();
|
||||
startInfo.UseShellExecute = false;
|
||||
startInfo.CreateNoWindow = true;
|
||||
|
||||
// Note: add a leading space to the --Args argument to avoid parsing as arguments
|
||||
if (isWindows)
|
||||
{
|
||||
startInfo.Arguments = $"--Path \"{installLocation}\" --Type \"{exe}\" --Args \" {args}\"";
|
||||
startInfo.Arguments = $"--Path \"{installLocation}\" --Type \"{appType}\" --Args \" {args}\"";
|
||||
startInfo.FileName = Path.Combine(updaterExePath);
|
||||
}
|
||||
else
|
||||
@@ -281,13 +299,12 @@ namespace Jackett.Common.Services
|
||||
args = exe + " " + args;
|
||||
exe = "mono";
|
||||
|
||||
startInfo.Arguments = $"{Path.Combine(updaterExePath)} --Path \"{installLocation}\" --Type \"{exe}\" --Args \" {args}\"";
|
||||
startInfo.Arguments = $"{Path.Combine(updaterExePath)} --Path \"{installLocation}\" --Type \"{appType}\" --Args \" {args}\"";
|
||||
startInfo.FileName = "mono";
|
||||
startInfo.UseShellExecute = false;
|
||||
startInfo.CreateNoWindow = true;
|
||||
}
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
var pid = Process.GetCurrentProcess().Id;
|
||||
startInfo.Arguments += $" --KillPids \"{pid}\"";
|
||||
}
|
||||
@@ -298,15 +315,30 @@ namespace Jackett.Common.Services
|
||||
}
|
||||
|
||||
if (NoRestart)
|
||||
{
|
||||
startInfo.Arguments += " --NoRestart";
|
||||
}
|
||||
|
||||
if (trayIsRunning && appType == "Console")
|
||||
{
|
||||
startInfo.Arguments += " --StartTray";
|
||||
}
|
||||
|
||||
logger.Info($"Starting updater: {startInfo.FileName} {startInfo.Arguments}");
|
||||
var procInfo = Process.Start(startInfo);
|
||||
logger.Info($"Updater started process id: {procInfo.Id}");
|
||||
if (NoRestart == false)
|
||||
{
|
||||
|
||||
if (!NoRestart)
|
||||
{
|
||||
if (isWindows)
|
||||
{
|
||||
logger.Info("Signal sent to lock service");
|
||||
lockService.Signal();
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
|
||||
logger.Info("Exiting Jackett..");
|
||||
lockService.Signal();
|
||||
|
||||
//TODO: Remove once off Owin
|
||||
if (EnvironmentUtil.IsRunningLegacyOwin)
|
||||
{
|
||||
|
119
src/Jackett.Common/Services/WindowsServiceConfigService.cs
Normal file
119
src/Jackett.Common/Services/WindowsServiceConfigService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.ServiceProcess;
|
||||
|
||||
namespace Jackett.Common.Services
|
||||
{
|
||||
public class WindowsServiceConfigService : IServiceConfigService
|
||||
{
|
||||
private const string NAME = "Jackett";
|
||||
private const string DESCRIPTION = "API Support for your favorite torrent trackers";
|
||||
private const string SERVICEEXE = "JackettService.exe";
|
||||
|
||||
private IProcessService processService;
|
||||
private Logger logger;
|
||||
|
||||
public WindowsServiceConfigService(IProcessService p, Logger l)
|
||||
{
|
||||
processService = p;
|
||||
logger = l;
|
||||
}
|
||||
|
||||
public bool ServiceExists()
|
||||
{
|
||||
return GetService(NAME) != null;
|
||||
}
|
||||
|
||||
public bool ServiceRunning()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
if (service == null)
|
||||
return false;
|
||||
return service.Status == ServiceControllerStatus.Running;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
service.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
service.Stop();
|
||||
}
|
||||
|
||||
public ServiceController GetService(string serviceName)
|
||||
{
|
||||
return ServiceController.GetServices().FirstOrDefault(c => String.Equals(c.ServiceName, serviceName, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public void Install()
|
||||
{
|
||||
if (ServiceExists())
|
||||
{
|
||||
logger.Warn("The service is already installed!");
|
||||
}
|
||||
else
|
||||
{
|
||||
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
|
||||
|
||||
var exePath = Path.Combine(applicationFolder, SERVICEEXE);
|
||||
if (!File.Exists(exePath) && Debugger.IsAttached)
|
||||
{
|
||||
exePath = Path.Combine(applicationFolder, "..\\..\\..\\Jackett.Service\\bin\\Debug", SERVICEEXE);
|
||||
}
|
||||
|
||||
string arg = $"create {NAME} start= auto binpath= \"{exePath}\" DisplayName= {NAME}";
|
||||
|
||||
processService.StartProcessAndLog("sc.exe", arg, true);
|
||||
|
||||
processService.StartProcessAndLog("sc.exe", $"description {NAME} \"{DESCRIPTION}\"", true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
RemoveService();
|
||||
|
||||
processService.StartProcessAndLog("sc.exe", $"delete {NAME}", true);
|
||||
|
||||
logger.Info("The service was uninstalled.");
|
||||
}
|
||||
|
||||
public void RemoveService()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
if (service == null)
|
||||
{
|
||||
logger.Warn("The service is already uninstalled");
|
||||
return;
|
||||
}
|
||||
if (service.Status != ServiceControllerStatus.Stopped)
|
||||
{
|
||||
service.Stop();
|
||||
service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60));
|
||||
|
||||
service.Refresh();
|
||||
if (service.Status == ServiceControllerStatus.Stopped)
|
||||
{
|
||||
logger.Info("Service stopped.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to stop the service");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("The service was already stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,17 +32,7 @@ namespace Jackett.Common.Utils
|
||||
|
||||
try
|
||||
{
|
||||
var currentAssembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
bool aspNetCorePresent = new StackTrace().GetFrames()
|
||||
.Select(x => x.GetMethod().ReflectedType.Assembly).Distinct()
|
||||
.Where(x => x.GetReferencedAssemblies().Any(y => y.FullName == currentAssembly.FullName))
|
||||
.Where(x => x.ManifestModule.Name == "JackettConsole.exe").Select(x => x.CustomAttributes)
|
||||
.FirstOrDefault()
|
||||
.Where(x => x.AttributeType.Assembly.FullName.StartsWith("Microsoft.AspNetCore", StringComparison.OrdinalIgnoreCase))
|
||||
.Any();
|
||||
|
||||
runningOwin = !aspNetCorePresent;
|
||||
runningOwin = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName.StartsWith("Jackett, ")).Any();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
62
src/Jackett.Common/Utils/LoggingSetup.cs
Normal file
62
src/Jackett.Common/Utils/LoggingSetup.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services;
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.LayoutRenderers;
|
||||
using NLog.Targets;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Jackett.Common.Utils
|
||||
{
|
||||
public static class LoggingSetup
|
||||
{
|
||||
public static LoggingConfiguration GetLoggingConfiguration(RuntimeSettings settings, bool fileOnly = false)
|
||||
{
|
||||
var logFileName = settings.CustomLogFileName ?? "log.txt";
|
||||
var logLevel = settings.TracingEnabled ? NLog.LogLevel.Debug : NLog.LogLevel.Info;
|
||||
// Add custom date time format renderer as the default is too long
|
||||
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("simpledatetime", typeof(SimpleDateTimeRenderer));
|
||||
|
||||
var logConfig = new LoggingConfiguration();
|
||||
var logFile = new FileTarget();
|
||||
logConfig.AddTarget("file", logFile);
|
||||
logFile.Layout = "${longdate} ${level} ${message} ${exception:format=ToString}";
|
||||
logFile.FileName = Path.Combine(settings.DataFolder, logFileName);
|
||||
logFile.ArchiveFileName = Path.Combine(settings.DataFolder, logFileName + ".{#####}.txt");
|
||||
logFile.ArchiveAboveSize = 500000;
|
||||
logFile.MaxArchiveFiles = 5;
|
||||
logFile.KeepFileOpen = false;
|
||||
logFile.ArchiveNumbering = ArchiveNumberingMode.DateAndSequence;
|
||||
var logFileRule = new LoggingRule("*", logLevel, logFile);
|
||||
logConfig.LoggingRules.Add(logFileRule);
|
||||
|
||||
if (!fileOnly)
|
||||
{
|
||||
var logConsole = new ColoredConsoleTarget();
|
||||
logConfig.AddTarget("console", logConsole);
|
||||
|
||||
logConsole.Layout = "${simpledatetime} ${level} ${message} ${exception:format=ToString}";
|
||||
var logConsoleRule = new LoggingRule("*", logLevel, logConsole);
|
||||
logConfig.LoggingRules.Add(logConsoleRule);
|
||||
|
||||
var logService = new LogCacheService();
|
||||
logConfig.AddTarget("service", logService);
|
||||
var serviceRule = new LoggingRule("*", logLevel, logService);
|
||||
logConfig.LoggingRules.Add(serviceRule);
|
||||
}
|
||||
|
||||
return logConfig;
|
||||
}
|
||||
|
||||
[LayoutRenderer("simpledatetime")]
|
||||
public class SimpleDateTimeRenderer : LayoutRenderer
|
||||
{
|
||||
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
|
||||
{
|
||||
builder.Append(DateTime.Now.ToString("MM-dd HH:mm:ss"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
98
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
98
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jackett.Server.Controllers
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[Route("bh/{indexerID}")]
|
||||
public class BlackholeController : Controller
|
||||
{
|
||||
private Logger logger;
|
||||
private IIndexerManagerService indexerService;
|
||||
private readonly ServerConfig serverConfig;
|
||||
private IProtectionService protectionService;
|
||||
|
||||
public BlackholeController(IIndexerManagerService i, Logger l, ServerConfig config, IProtectionService ps)
|
||||
{
|
||||
logger = l;
|
||||
indexerService = i;
|
||||
serverConfig = config;
|
||||
protectionService = ps;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Blackhole(string indexerID, string path, string jackett_apikey, string file)
|
||||
{
|
||||
var jsonReply = new JObject();
|
||||
try
|
||||
{
|
||||
var indexer = indexerService.GetWebIndexer(indexerID);
|
||||
if (!indexer.IsConfigured)
|
||||
{
|
||||
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
|
||||
throw new Exception("This indexer is not configured.");
|
||||
}
|
||||
|
||||
if (serverConfig.APIKey != jackett_apikey)
|
||||
throw new Exception("Incorrect API key");
|
||||
|
||||
path = WebUtility.UrlDecode(path);
|
||||
path = protectionService.UnProtect(path);
|
||||
var remoteFile = new Uri(path, UriKind.RelativeOrAbsolute);
|
||||
var fileExtension = ".torrent";
|
||||
var downloadBytes = await indexer.Download(remoteFile);
|
||||
|
||||
// handle magnet URLs
|
||||
if (downloadBytes.Length >= 7
|
||||
&& downloadBytes[0] == 0x6d // m
|
||||
&& downloadBytes[1] == 0x61 // a
|
||||
&& downloadBytes[2] == 0x67 // g
|
||||
&& downloadBytes[3] == 0x6e // n
|
||||
&& downloadBytes[4] == 0x65 // e
|
||||
&& downloadBytes[5] == 0x74 // t
|
||||
&& downloadBytes[6] == 0x3a // :
|
||||
)
|
||||
{
|
||||
fileExtension = ".magnet";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serverConfig.BlackholeDir))
|
||||
{
|
||||
throw new Exception("Blackhole directory not set!");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(serverConfig.BlackholeDir))
|
||||
{
|
||||
throw new Exception("Blackhole directory does not exist: " + serverConfig.BlackholeDir);
|
||||
}
|
||||
|
||||
var fileName = DateTime.Now.Ticks.ToString() + "-" + StringUtil.MakeValidFileName(indexer.DisplayName, '_', false);
|
||||
if (string.IsNullOrWhiteSpace(file))
|
||||
fileName += fileExtension;
|
||||
else
|
||||
fileName += "-" + StringUtil.MakeValidFileName(file + fileExtension, '_', false); // call MakeValidFileName() again to avoid any possibility of path traversal attacks
|
||||
|
||||
System.IO.File.WriteAllBytes(Path.Combine(serverConfig.BlackholeDir, fileName), downloadBytes);
|
||||
jsonReply["result"] = "success";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Error downloading to blackhole " + indexerID + " " + path);
|
||||
jsonReply["result"] = "error";
|
||||
jsonReply["error"] = ex.Message;
|
||||
}
|
||||
|
||||
return Json(jsonReply);
|
||||
}
|
||||
}
|
||||
}
|
96
src/Jackett.Server/Controllers/DownloadController.cs
Normal file
96
src/Jackett.Server/Controllers/DownloadController.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using BencodeNET.Parsing;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jackett.Server.Controllers
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[Route("dl/{indexerID}")]
|
||||
public class DownloadController : Controller
|
||||
{
|
||||
private ServerConfig config;
|
||||
private Logger logger;
|
||||
private IIndexerManagerService indexerService;
|
||||
private IProtectionService protectionService;
|
||||
|
||||
public DownloadController(IIndexerManagerService i, Logger l, IProtectionService ps, ServerConfig serverConfig)
|
||||
{
|
||||
config = serverConfig;
|
||||
logger = l;
|
||||
indexerService = i;
|
||||
protectionService = ps;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Download(string indexerID, string path, string jackett_apikey, string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
var indexer = indexerService.GetWebIndexer(indexerID);
|
||||
|
||||
if (!indexer.IsConfigured)
|
||||
{
|
||||
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
|
||||
return Forbid("This indexer is not configured.");
|
||||
}
|
||||
|
||||
path = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(path));
|
||||
path = protectionService.UnProtect(path);
|
||||
|
||||
if (config.APIKey != jackett_apikey)
|
||||
return Unauthorized();
|
||||
|
||||
var target = new Uri(path, UriKind.RelativeOrAbsolute);
|
||||
var downloadBytes = await indexer.Download(target);
|
||||
|
||||
// handle magnet URLs
|
||||
if (downloadBytes.Length >= 7
|
||||
&& downloadBytes[0] == 0x6d // m
|
||||
&& downloadBytes[1] == 0x61 // a
|
||||
&& downloadBytes[2] == 0x67 // g
|
||||
&& downloadBytes[3] == 0x6e // n
|
||||
&& downloadBytes[4] == 0x65 // e
|
||||
&& downloadBytes[5] == 0x74 // t
|
||||
&& downloadBytes[6] == 0x3a // :
|
||||
)
|
||||
{
|
||||
var magneturi = Encoding.UTF8.GetString(downloadBytes);
|
||||
return Redirect(new Uri(magneturi).ToString());
|
||||
}
|
||||
|
||||
// This will fix torrents where the keys are not sorted, and thereby not supported by Sonarr.
|
||||
byte[] sortedDownloadBytes = null;
|
||||
try
|
||||
{
|
||||
var parser = new BencodeParser();
|
||||
var torrentDictionary = parser.Parse(downloadBytes);
|
||||
sortedDownloadBytes = torrentDictionary.EncodeAsBytes();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var content = indexer.Encoding.GetString(downloadBytes);
|
||||
logger.Error(content);
|
||||
throw new Exception("BencodeParser failed", e);
|
||||
}
|
||||
|
||||
string fileName = StringUtil.MakeValidFileName(file, '_', false) + ".torrent"; // call MakeValidFileName again to avoid any kind of injection attack
|
||||
|
||||
return File(sortedDownloadBytes, "application/x-bittorrent", fileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error downloading " + indexerID + " " + path);
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
178
src/Jackett.Server/Controllers/IndexerApiController.cs
Normal file
178
src/Jackett.Server/Controllers/IndexerApiController.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using Jackett.Common.Indexers;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jackett.Server.Controllers
|
||||
{
|
||||
public interface IIndexerController
|
||||
{
|
||||
IIndexerManagerService IndexerService { get; }
|
||||
IIndexer CurrentIndexer { get; set; }
|
||||
}
|
||||
|
||||
public class RequiresIndexer : IActionFilter
|
||||
{
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var controller = context.Controller;
|
||||
if (!(controller is IIndexerController))
|
||||
return;
|
||||
|
||||
var indexerController = controller as IIndexerController;
|
||||
|
||||
var parameters = context.RouteData.Values;
|
||||
|
||||
if (!parameters.ContainsKey("indexerId"))
|
||||
{
|
||||
indexerController.CurrentIndexer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var indexerId = parameters["indexerId"] as string;
|
||||
if (indexerId.IsNullOrEmptyOrWhitespace())
|
||||
return;
|
||||
|
||||
var indexerService = indexerController.IndexerService;
|
||||
var indexer = indexerService.GetIndexer(indexerId);
|
||||
indexerController.CurrentIndexer = indexer;
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
// do something after the action executes
|
||||
}
|
||||
}
|
||||
|
||||
[Route("api/v2.0/indexers")]
|
||||
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class IndexerApiController : Controller, IIndexerController
|
||||
{
|
||||
public IIndexerManagerService IndexerService { get; private set; }
|
||||
public IIndexer CurrentIndexer { get; set; }
|
||||
private Logger logger;
|
||||
private IServerService serverService;
|
||||
private ICacheService cacheService;
|
||||
|
||||
public IndexerApiController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger)
|
||||
{
|
||||
IndexerService = indexerManagerService;
|
||||
serverService = ss;
|
||||
cacheService = c;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[TypeFilter(typeof(RequiresIndexer))]
|
||||
[Route("{indexerId?}/Config")]
|
||||
public async Task<IActionResult> Config()
|
||||
{
|
||||
var config = await CurrentIndexer.GetConfigurationForSetup();
|
||||
return Ok(config.ToJson(null));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{indexerId?}/Config")]
|
||||
[TypeFilter(typeof(RequiresIndexer))]
|
||||
public async Task<IActionResult> UpdateConfig([FromBody]Common.Models.DTO.ConfigItem[] config)
|
||||
{
|
||||
try
|
||||
{
|
||||
// HACK
|
||||
var jsonString = JsonConvert.SerializeObject(config);
|
||||
var json = JToken.Parse(jsonString);
|
||||
|
||||
var configurationResult = await CurrentIndexer.ApplyConfiguration(json);
|
||||
|
||||
if (configurationResult == IndexerConfigurationStatus.RequiresTesting)
|
||||
{
|
||||
await IndexerService.TestIndexer(CurrentIndexer.ID);
|
||||
}
|
||||
|
||||
return new NoContentResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
var baseIndexer = CurrentIndexer as BaseIndexer;
|
||||
if (null != baseIndexer)
|
||||
baseIndexer.ResetBaseConfig();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public IEnumerable<Common.Models.DTO.Indexer> Indexers()
|
||||
{
|
||||
var dto = IndexerService.GetAllIndexers().Select(i => new Common.Models.DTO.Indexer(i));
|
||||
return dto;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{indexerid}/[action]")]
|
||||
[TypeFilter(typeof(RequiresIndexer))]
|
||||
public async Task<IActionResult> Test()
|
||||
{
|
||||
JToken jsonReply = new JObject();
|
||||
try
|
||||
{
|
||||
await IndexerService.TestIndexer(CurrentIndexer.ID);
|
||||
CurrentIndexer.LastError = null;
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = ex.Message;
|
||||
if (ex.InnerException != null)
|
||||
msg += ": " + ex.InnerException.Message;
|
||||
|
||||
if (CurrentIndexer != null)
|
||||
CurrentIndexer.LastError = msg;
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[TypeFilter(typeof(RequiresIndexer))]
|
||||
[Route("{indexerid}")]
|
||||
public void Delete()
|
||||
{
|
||||
IndexerService.DeleteIndexer(CurrentIndexer.ID);
|
||||
}
|
||||
|
||||
// TODO
|
||||
// This should go to ServerConfigurationController
|
||||
[Route("Cache")]
|
||||
[HttpGet]
|
||||
public List<TrackerCacheResult> Cache()
|
||||
{
|
||||
var results = cacheService.GetCachedResults();
|
||||
ConfigureCacheResults(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
|
||||
{
|
||||
var serverUrl = serverService.GetServerUrl(Request);
|
||||
foreach (var result in results)
|
||||
{
|
||||
var link = result.Link;
|
||||
var file = StringUtil.MakeValidFileName(result.Title, '_', false);
|
||||
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
|
||||
if (result.Link != null && result.Link.Scheme != "magnet" && !string.IsNullOrWhiteSpace(serverService.GetBlackholeDirectory()))
|
||||
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
544
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
544
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
@@ -0,0 +1,544 @@
|
||||
using Jackett.Common;
|
||||
using Jackett.Common.Indexers;
|
||||
using Jackett.Common.Indexers.Meta;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.DTO;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Jackett.Server.Controllers
|
||||
{
|
||||
public class RequiresApiKey : IActionFilter
|
||||
{
|
||||
public IServerService serverService;
|
||||
|
||||
public RequiresApiKey(IServerService ss)
|
||||
{
|
||||
serverService = ss;
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var validApiKey = serverService.GetApiKey();
|
||||
var queryParams = context.HttpContext.Request.Query;
|
||||
var queryApiKey = queryParams.Where(x => x.Key == "apikey" || x.Key == "passkey").Select(x => x.Value).FirstOrDefault();
|
||||
|
||||
#if DEBUG
|
||||
if (Debugger.IsAttached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (queryApiKey != validApiKey)
|
||||
{
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.Unauthorized, 100, "Invalid API Key");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
// do something after the action executes
|
||||
}
|
||||
}
|
||||
|
||||
public class RequiresConfiguredIndexer : IActionFilter
|
||||
{
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var controller = context.Controller;
|
||||
if (!(controller is IIndexerController))
|
||||
return;
|
||||
|
||||
var indexerController = controller as IIndexerController;
|
||||
|
||||
var parameters = context.RouteData.Values;
|
||||
|
||||
if (!parameters.ContainsKey("indexerId"))
|
||||
{
|
||||
indexerController.CurrentIndexer = null;
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 200, "Indexer is not specified");
|
||||
return;
|
||||
}
|
||||
|
||||
var indexerId = parameters["indexerId"] as string;
|
||||
if (indexerId.IsNullOrEmptyOrWhitespace())
|
||||
{
|
||||
indexerController.CurrentIndexer = null;
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not specified (empty value)");
|
||||
return;
|
||||
}
|
||||
|
||||
var indexerService = indexerController.IndexerService;
|
||||
var indexer = indexerService.GetIndexer(indexerId);
|
||||
|
||||
if (indexer == null)
|
||||
{
|
||||
indexerController.CurrentIndexer = null;
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!indexer.IsConfigured)
|
||||
{
|
||||
indexerController.CurrentIndexer = null;
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
indexerController.CurrentIndexer = indexer;
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
// do something after the action executes
|
||||
}
|
||||
}
|
||||
|
||||
public class RequiresValidQuery : IActionFilter
|
||||
{
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
//TODO: Not sure what this is meant to do
|
||||
//if (context.HttpContext.Response != null)
|
||||
// return;
|
||||
|
||||
var controller = context.Controller;
|
||||
if (!(controller is IResultController))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var resultController = controller as IResultController;
|
||||
|
||||
var query = context.ActionArguments.First().Value;
|
||||
var queryType = query.GetType();
|
||||
var converter = queryType.GetMethod("ToTorznabQuery", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
|
||||
if (converter == null)
|
||||
{
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.BadRequest, 900, "ToTorznabQuery() not found");
|
||||
}
|
||||
|
||||
var converted = converter.Invoke(null, new object[] { query });
|
||||
var torznabQuery = converted as TorznabQuery;
|
||||
resultController.CurrentQuery = torznabQuery;
|
||||
|
||||
if (queryType == typeof(ApiSearch)) // Skip CanHandleQuery() check for manual search (CurrentIndexer isn't used during manul search)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resultController.CurrentIndexer.CanHandleQuery(resultController.CurrentQuery))
|
||||
{
|
||||
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.BadRequest, 201, $"{resultController.CurrentIndexer.ID} " +
|
||||
$"does not support the requested query. Please check the capabilities (t=caps) and make sure the search mode and categories are supported.");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
// do something after the action executes
|
||||
}
|
||||
}
|
||||
|
||||
public interface IResultController : IIndexerController
|
||||
{
|
||||
TorznabQuery CurrentQuery { get; set; }
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[Route("api/v2.0/indexers/{indexerId}/results")]
|
||||
[TypeFilter(typeof(RequiresApiKey))]
|
||||
[TypeFilter(typeof(RequiresConfiguredIndexer))]
|
||||
[TypeFilter(typeof(RequiresValidQuery))]
|
||||
public class ResultsController : Controller, IResultController
|
||||
{
|
||||
public IIndexerManagerService IndexerService { get; private set; }
|
||||
public IIndexer CurrentIndexer { get; set; }
|
||||
public TorznabQuery CurrentQuery { get; set; }
|
||||
private Logger logger;
|
||||
private IServerService serverService;
|
||||
private ICacheService cacheService;
|
||||
private Common.Models.Config.ServerConfig serverConfig;
|
||||
|
||||
public ResultsController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger, Common.Models.Config.ServerConfig sConfig)
|
||||
{
|
||||
IndexerService = indexerManagerService;
|
||||
serverService = ss;
|
||||
cacheService = c;
|
||||
this.logger = logger;
|
||||
serverConfig = sConfig;
|
||||
}
|
||||
|
||||
[Route("")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Results([FromQuery] ApiSearch requestt)
|
||||
{
|
||||
//TODO: Better way to parse querystring
|
||||
|
||||
ApiSearch request = new ApiSearch();
|
||||
|
||||
foreach (var t in Request.Query)
|
||||
{
|
||||
if (t.Key == "Tracker[]")
|
||||
{
|
||||
request.Tracker = t.Value.ToString().Split(',');
|
||||
}
|
||||
|
||||
if (t.Key == "Category[]")
|
||||
{
|
||||
request.Category = t.Value.ToString().Split(',').Select(Int32.Parse).ToArray();
|
||||
}
|
||||
|
||||
if (t.Key == "query")
|
||||
{
|
||||
request.Query = t.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
var manualResult = new ManualSearchResult();
|
||||
var trackers = IndexerService.GetAllIndexers().ToList().Where(t => t.IsConfigured);
|
||||
if (request.Tracker != null)
|
||||
{
|
||||
trackers = trackers.Where(t => request.Tracker.Contains(t.ID));
|
||||
}
|
||||
|
||||
trackers = trackers.Where(t => t.CanHandleQuery(CurrentQuery));
|
||||
|
||||
var tasks = trackers.ToList().Select(t => t.ResultsForQuery(CurrentQuery)).ToList();
|
||||
try
|
||||
{
|
||||
var aggregateTask = Task.WhenAll(tasks);
|
||||
await aggregateTask;
|
||||
}
|
||||
catch (AggregateException aex)
|
||||
{
|
||||
foreach (var ex in aex.InnerExceptions)
|
||||
{
|
||||
logger.Error(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
}
|
||||
|
||||
manualResult.Indexers = tasks.Select(t =>
|
||||
{
|
||||
var resultIndexer = new ManualSearchResultIndexer();
|
||||
IIndexer indexer = null;
|
||||
if (t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
resultIndexer.Status = ManualSearchResultIndexerStatus.OK;
|
||||
resultIndexer.Results = t.Result.Releases.Count();
|
||||
resultIndexer.Error = null;
|
||||
indexer = t.Result.Indexer;
|
||||
}
|
||||
else if (t.Exception.InnerException is IndexerException)
|
||||
{
|
||||
resultIndexer.Status = ManualSearchResultIndexerStatus.Error;
|
||||
resultIndexer.Results = 0;
|
||||
resultIndexer.Error = ((IndexerException)t.Exception.InnerException).ToString();
|
||||
indexer = ((IndexerException)t.Exception.InnerException).Indexer;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultIndexer.Status = ManualSearchResultIndexerStatus.Unknown;
|
||||
resultIndexer.Results = 0;
|
||||
resultIndexer.Error = null;
|
||||
}
|
||||
|
||||
if (indexer != null)
|
||||
{
|
||||
resultIndexer.ID = indexer.ID;
|
||||
resultIndexer.Name = indexer.DisplayName;
|
||||
}
|
||||
return resultIndexer;
|
||||
}).ToList();
|
||||
|
||||
manualResult.Results = tasks.Where(t => t.Status == TaskStatus.RanToCompletion).Where(t => t.Result.Releases.Any()).SelectMany(t =>
|
||||
{
|
||||
var searchResults = t.Result.Releases;
|
||||
var indexer = t.Result.Indexer;
|
||||
cacheService.CacheRssResults(indexer, searchResults);
|
||||
|
||||
return searchResults.Select(result =>
|
||||
{
|
||||
var item = AutoMapper.Mapper.Map<TrackerCacheResult>(result);
|
||||
item.Tracker = indexer.DisplayName;
|
||||
item.TrackerId = indexer.ID;
|
||||
item.Peers = item.Peers - item.Seeders; // Use peers as leechers
|
||||
|
||||
return item;
|
||||
});
|
||||
}).OrderByDescending(d => d.PublishDate).ToList();
|
||||
|
||||
ConfigureCacheResults(manualResult.Results);
|
||||
|
||||
logger.Info(string.Format("Manual search for \"{0}\" on {1} with {2} results.", CurrentQuery.SanitizedSearchTerm, string.Join(", ", manualResult.Indexers.Select(i => i.ID)), manualResult.Results.Count()));
|
||||
return Json(manualResult);
|
||||
}
|
||||
|
||||
[Route("[action]/{ignored?}")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Torznab([FromQuery]TorznabRequest request)
|
||||
{
|
||||
if (string.Equals(CurrentQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return Content(CurrentIndexer.TorznabCaps.ToXml(), "application/rss+xml", Encoding.UTF8);
|
||||
}
|
||||
|
||||
// indexers - returns a list of all included indexers (meta indexers only)
|
||||
if (string.Equals(CurrentQuery.QueryType, "indexers", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (!(CurrentIndexer is BaseMetaIndexer)) // shouldn't be needed because CanHandleQuery should return false
|
||||
{
|
||||
logger.Warn($"A search request with t=indexers from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} isn't a meta indexer.");
|
||||
return GetErrorXML(203, "Function Not Available: this isn't a meta indexer");
|
||||
}
|
||||
var CurrentBaseMetaIndexer = (BaseMetaIndexer)CurrentIndexer;
|
||||
var indexers = CurrentBaseMetaIndexer.Indexers;
|
||||
if (string.Equals(request.configured, "true", StringComparison.InvariantCultureIgnoreCase))
|
||||
indexers = indexers.Where(i => i.IsConfigured);
|
||||
else if (string.Equals(request.configured, "false", StringComparison.InvariantCultureIgnoreCase))
|
||||
indexers = indexers.Where(i => !i.IsConfigured);
|
||||
|
||||
var xdoc = new XDocument(
|
||||
new XDeclaration("1.0", "UTF-8", null),
|
||||
new XElement("indexers",
|
||||
from i in indexers
|
||||
select new XElement("indexer",
|
||||
new XAttribute("id", i.ID),
|
||||
new XAttribute("configured", i.IsConfigured),
|
||||
new XElement("title", i.DisplayName),
|
||||
new XElement("description", i.DisplayDescription),
|
||||
new XElement("link", i.SiteLink),
|
||||
new XElement("language", i.Language),
|
||||
new XElement("type", i.Type),
|
||||
i.TorznabCaps.GetXDocument().FirstNode
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return Content(xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString(), "application/xml", Encoding.UTF8);
|
||||
}
|
||||
|
||||
if (CurrentQuery.ImdbID != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(CurrentQuery.SearchTerm))
|
||||
{
|
||||
logger.Warn($"A search request from {Request.HttpContext.Connection.RemoteIpAddress} was made containing q and imdbid.");
|
||||
return GetErrorXML(201, "Incorrect parameter: please specify either imdbid or q");
|
||||
}
|
||||
|
||||
CurrentQuery.ImdbID = ParseUtil.GetFullImdbID(CurrentQuery.ImdbID); // normalize ImdbID
|
||||
if (CurrentQuery.ImdbID == null)
|
||||
{
|
||||
logger.Warn($"A search request from {Request.HttpContext.Connection.RemoteIpAddress} was made with an invalid imdbid.");
|
||||
return GetErrorXML(201, "Incorrect parameter: invalid imdbid format");
|
||||
}
|
||||
|
||||
if (!CurrentIndexer.TorznabCaps.SupportsImdbSearch)
|
||||
{
|
||||
logger.Warn($"A search request with imdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} doesn't support it.");
|
||||
return GetErrorXML(203, "Function Not Available: imdbid is not supported by this indexer");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CurrentIndexer.ResultsForQuery(CurrentQuery);
|
||||
|
||||
// Some trackers do not support multiple category filtering so filter the releases that match manually.
|
||||
int? newItemCount = null;
|
||||
|
||||
// Cache non query results
|
||||
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
|
||||
{
|
||||
newItemCount = cacheService.GetNewItemCount(CurrentIndexer, result.Releases);
|
||||
cacheService.CacheRssResults(CurrentIndexer, result.Releases);
|
||||
}
|
||||
|
||||
// Log info
|
||||
var logBuilder = new StringBuilder();
|
||||
if (newItemCount != null)
|
||||
{
|
||||
logBuilder.AppendFormat("Found {0} ({1} new) releases from {2}", result.Releases.Count(), newItemCount, CurrentIndexer.DisplayName);
|
||||
}
|
||||
else
|
||||
{
|
||||
logBuilder.AppendFormat("Found {0} releases from {1}", result.Releases.Count(), CurrentIndexer.DisplayName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
|
||||
{
|
||||
logBuilder.AppendFormat(" for: {0}", CurrentQuery.GetQueryString());
|
||||
}
|
||||
|
||||
logger.Info(logBuilder.ToString());
|
||||
|
||||
var serverUrl = serverService.GetServerUrl(Request);
|
||||
var resultPage = new ResultPage(new ChannelInfo
|
||||
{
|
||||
Title = CurrentIndexer.DisplayName,
|
||||
Description = CurrentIndexer.DisplayDescription,
|
||||
Link = new Uri(CurrentIndexer.SiteLink),
|
||||
ImageUrl = new Uri(serverUrl + "logos/" + CurrentIndexer.ID + ".png"),
|
||||
ImageTitle = CurrentIndexer.DisplayName,
|
||||
ImageLink = new Uri(CurrentIndexer.SiteLink),
|
||||
ImageDescription = CurrentIndexer.DisplayName
|
||||
});
|
||||
|
||||
var proxiedReleases = result.Releases.Select(r => AutoMapper.Mapper.Map<ReleaseInfo>(r)).Select(r =>
|
||||
{
|
||||
r.Link = serverService.ConvertToProxyLink(r.Link, serverUrl, r.Origin.ID, "dl", r.Title);
|
||||
return r;
|
||||
});
|
||||
|
||||
resultPage.Releases = proxiedReleases.ToList();
|
||||
|
||||
var xml = resultPage.ToXml(new Uri(serverUrl));
|
||||
// Force the return as XML
|
||||
|
||||
return Content(xml, "application/rss+xml", Encoding.UTF8);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex);
|
||||
return GetErrorXML(900, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
[Route("[action]/{ignored?}")]
|
||||
public IActionResult GetErrorXML(int code, string description)
|
||||
{
|
||||
return Content(CreateErrorXML(code, description), "application/xml", Encoding.UTF8);
|
||||
}
|
||||
|
||||
public static string CreateErrorXML(int code, string description)
|
||||
{
|
||||
var xdoc = new XDocument(
|
||||
new XDeclaration("1.0", "UTF-8", null),
|
||||
new XElement("error",
|
||||
new XAttribute("code", code.ToString()),
|
||||
new XAttribute("description", description)
|
||||
)
|
||||
);
|
||||
return xdoc.Declaration + Environment.NewLine + xdoc;
|
||||
}
|
||||
|
||||
public static IActionResult GetErrorActionResult(RouteData routeData, HttpStatusCode status, int torznabCode, string description)
|
||||
{
|
||||
bool isTorznab = routeData.Values["action"].ToString().Equals("torznab", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isTorznab)
|
||||
{
|
||||
ContentResult contentResult = new ContentResult
|
||||
{
|
||||
Content = CreateErrorXML(torznabCode, description),
|
||||
ContentType = "application/xml",
|
||||
StatusCode = 200
|
||||
};
|
||||
return contentResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
return new UnauthorizedResult();
|
||||
case HttpStatusCode.NotFound:
|
||||
return new NotFoundObjectResult(description);
|
||||
case HttpStatusCode.BadRequest:
|
||||
return new BadRequestObjectResult(description);
|
||||
default:
|
||||
return new ContentResult
|
||||
{
|
||||
Content = description,
|
||||
StatusCode = (int)status
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Route("[action]/{ignored?}")]
|
||||
[HttpGet]
|
||||
public async Task<TorrentPotatoResponse> Potato([FromQuery]TorrentPotatoRequest request)
|
||||
{
|
||||
var result = await CurrentIndexer.ResultsForQuery(CurrentQuery);
|
||||
|
||||
// Cache non query results
|
||||
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
|
||||
cacheService.CacheRssResults(CurrentIndexer, result.Releases);
|
||||
|
||||
// Log info
|
||||
if (string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
|
||||
logger.Info($"Found {result.Releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName}");
|
||||
else
|
||||
logger.Info($"Found {result.Releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName} for: {CurrentQuery.GetQueryString()}");
|
||||
|
||||
var serverUrl = serverService.GetServerUrl(Request);
|
||||
var potatoReleases = result.Releases.Where(r => r.Link != null || r.MagnetUri != null).Select(r =>
|
||||
{
|
||||
var release = AutoMapper.Mapper.Map<ReleaseInfo>(r);
|
||||
release.Link = serverService.ConvertToProxyLink(release.Link, serverUrl, CurrentIndexer.ID, "dl", release.Title);
|
||||
var item = new TorrentPotatoResponseItem()
|
||||
{
|
||||
release_name = release.Title + "[" + CurrentIndexer.DisplayName + "]", // Suffix the indexer so we can see which tracker we are using in CPS as it just says torrentpotato >.>
|
||||
torrent_id = release.Guid.ToString(),
|
||||
details_url = release.Comments.ToString(),
|
||||
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
|
||||
imdb_id = release.Imdb.HasValue ? ParseUtil.GetFullImdbID("tt" + release.Imdb) : null,
|
||||
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
|
||||
type = "movie",
|
||||
size = (long)release.Size / (1024 * 1024), // This is in MB
|
||||
leechers = (release.Peers ?? -1) - (release.Seeders ?? 0),
|
||||
seeders = release.Seeders ?? -1,
|
||||
publish_date = r.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s")
|
||||
};
|
||||
return item;
|
||||
});
|
||||
|
||||
var potatoResponse = new TorrentPotatoResponse()
|
||||
{
|
||||
results = potatoReleases.ToList()
|
||||
};
|
||||
|
||||
return potatoResponse;
|
||||
}
|
||||
|
||||
[Route("[action]/{ignored?}")]
|
||||
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
|
||||
{
|
||||
var serverUrl = serverService.GetServerUrl(Request);
|
||||
foreach (var result in results)
|
||||
{
|
||||
var link = result.Link;
|
||||
var file = StringUtil.MakeValidFileName(result.Title, '_', false);
|
||||
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
|
||||
if (!string.IsNullOrWhiteSpace(serverConfig.BlackholeDir))
|
||||
{
|
||||
if (result.Link != null)
|
||||
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
|
||||
else if (result.MagnetUri != null)
|
||||
result.BlackholeLink = serverService.ConvertToProxyLink(result.MagnetUri, serverUrl, result.TrackerId, "bh", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
209
src/Jackett.Server/Controllers/ServerConfigurationController.cs
Normal file
209
src/Jackett.Server/Controllers/ServerConfigurationController.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Jackett.Server.Controllers
|
||||
{
|
||||
[Route("api/v2.0/server/[action]")]
|
||||
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ServerConfigurationController : Controller
|
||||
{
|
||||
private readonly IConfigurationService configService;
|
||||
private ServerConfig serverConfig;
|
||||
private IServerService serverService;
|
||||
private IProcessService processService;
|
||||
private IIndexerManagerService indexerService;
|
||||
private ISecuityService securityService;
|
||||
private IUpdateService updater;
|
||||
private ILogCacheService logCache;
|
||||
private Logger logger;
|
||||
|
||||
public ServerConfigurationController(IConfigurationService c, IServerService s, IProcessService p, IIndexerManagerService i, ISecuityService ss, IUpdateService u, ILogCacheService lc, Logger l, ServerConfig sc)
|
||||
{
|
||||
configService = c;
|
||||
serverConfig = sc;
|
||||
serverService = s;
|
||||
processService = p;
|
||||
indexerService = i;
|
||||
securityService = ss;
|
||||
updater = u;
|
||||
logCache = lc;
|
||||
logger = l;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult AdminPassword([FromBody]string password)
|
||||
{
|
||||
var oldPassword = serverConfig.AdminPassword;
|
||||
if (string.IsNullOrEmpty(password))
|
||||
password = null;
|
||||
|
||||
if (oldPassword != password)
|
||||
{
|
||||
serverConfig.AdminPassword = securityService.HashPassword(password);
|
||||
configService.SaveConfig(serverConfig);
|
||||
}
|
||||
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public void Update()
|
||||
{
|
||||
updater.CheckForUpdatesNow();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public Common.Models.DTO.ServerConfig Config()
|
||||
{
|
||||
var dto = new Common.Models.DTO.ServerConfig(serverService.notices, serverConfig, configService.GetVersion());
|
||||
return dto;
|
||||
}
|
||||
|
||||
[ActionName("Config")]
|
||||
[HttpPost]
|
||||
public IActionResult UpdateConfig([FromBody]Common.Models.DTO.ServerConfig config)
|
||||
{
|
||||
bool webHostRestartNeeded = false;
|
||||
|
||||
var originalPort = serverConfig.Port;
|
||||
var originalAllowExternal = serverConfig.AllowExternal;
|
||||
int port = config.port;
|
||||
bool external = config.external;
|
||||
string saveDir = config.blackholedir;
|
||||
bool updateDisabled = config.updatedisabled;
|
||||
bool preRelease = config.prerelease;
|
||||
bool logging = config.logging;
|
||||
string basePathOverride = config.basepathoverride;
|
||||
if (basePathOverride != null)
|
||||
{
|
||||
basePathOverride = basePathOverride.TrimEnd('/');
|
||||
if (!string.IsNullOrWhiteSpace(basePathOverride) && !basePathOverride.StartsWith("/"))
|
||||
throw new Exception("The Base Path Override must start with a /");
|
||||
}
|
||||
|
||||
string omdbApiKey = config.omdbkey;
|
||||
string omdbApiUrl = config.omdburl;
|
||||
|
||||
if (config.basepathoverride != serverConfig.BasePathOverride)
|
||||
{
|
||||
webHostRestartNeeded = true;
|
||||
}
|
||||
|
||||
serverConfig.UpdateDisabled = updateDisabled;
|
||||
serverConfig.UpdatePrerelease = preRelease;
|
||||
serverConfig.BasePathOverride = basePathOverride;
|
||||
serverConfig.RuntimeSettings.BasePath = serverService.BasePath();
|
||||
configService.SaveConfig(serverConfig);
|
||||
|
||||
Helper.SetLogLevel(logging ? LogLevel.Debug : LogLevel.Info);
|
||||
serverConfig.RuntimeSettings.TracingEnabled = logging;
|
||||
|
||||
if (omdbApiKey != serverConfig.OmdbApiKey || omdbApiUrl != serverConfig.OmdbApiUrl)
|
||||
{
|
||||
serverConfig.OmdbApiKey = omdbApiKey;
|
||||
serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/');
|
||||
configService.SaveConfig(serverConfig);
|
||||
// HACK
|
||||
indexerService.InitAggregateIndexer();
|
||||
}
|
||||
|
||||
if (config.proxy_type != serverConfig.ProxyType ||
|
||||
config.proxy_url != serverConfig.ProxyUrl ||
|
||||
config.proxy_port != serverConfig.ProxyPort ||
|
||||
config.proxy_username != serverConfig.ProxyUsername ||
|
||||
config.proxy_password != serverConfig.ProxyPassword)
|
||||
{
|
||||
if (config.proxy_port < 1 || config.proxy_port > 65535)
|
||||
throw new Exception("The port you have selected is invalid, it must be below 65535.");
|
||||
|
||||
serverConfig.ProxyUrl = config.proxy_url;
|
||||
serverConfig.ProxyType = config.proxy_type;
|
||||
serverConfig.ProxyPort = config.proxy_port;
|
||||
serverConfig.ProxyUsername = config.proxy_username;
|
||||
serverConfig.ProxyPassword = config.proxy_password;
|
||||
configService.SaveConfig(serverConfig);
|
||||
}
|
||||
|
||||
if (port != serverConfig.Port || external != serverConfig.AllowExternal)
|
||||
{
|
||||
if (ServerUtil.RestrictedPorts.Contains(port))
|
||||
throw new Exception("The port you have selected is restricted, try a different one.");
|
||||
|
||||
if (port < 1 || port > 65535)
|
||||
throw new Exception("The port you have selected is invalid, it must be below 65535.");
|
||||
|
||||
// Save port to the config so it can be picked up by the if needed when running as admin below.
|
||||
serverConfig.AllowExternal = external;
|
||||
serverConfig.Port = port;
|
||||
configService.SaveConfig(serverConfig);
|
||||
|
||||
// On Windows change the url reservations
|
||||
if (System.Environment.OSVersion.Platform != PlatformID.Unix)
|
||||
{
|
||||
if (!ServerUtil.IsUserAdministrator())
|
||||
{
|
||||
try
|
||||
{
|
||||
var consoleExePath = System.Reflection.Assembly.GetExecutingAssembly().CodeBase.Replace(".dll", ".exe");
|
||||
processService.StartProcessAndLog(consoleExePath, "--ReserveUrls", true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
serverConfig.Port = originalPort;
|
||||
serverConfig.AllowExternal = originalAllowExternal;
|
||||
configService.SaveConfig(serverConfig);
|
||||
|
||||
throw new Exception("Failed to acquire admin permissions to reserve the new port.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
serverService.ReserveUrls(true);
|
||||
}
|
||||
}
|
||||
|
||||
webHostRestartNeeded = true;
|
||||
}
|
||||
|
||||
if (saveDir != serverConfig.BlackholeDir)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(saveDir))
|
||||
{
|
||||
if (!Directory.Exists(saveDir))
|
||||
{
|
||||
throw new Exception("Blackhole directory does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
serverConfig.BlackholeDir = saveDir;
|
||||
configService.SaveConfig(serverConfig);
|
||||
}
|
||||
|
||||
if (webHostRestartNeeded)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
logger.Info("Restarting webhost due to configuration change");
|
||||
Helper.RestartWebHost();
|
||||
}
|
||||
|
||||
serverConfig.ConfigChanged();
|
||||
|
||||
return Json(serverConfig);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<CachedLog> Logs()
|
||||
{
|
||||
return logCache.Logs;
|
||||
}
|
||||
}
|
||||
}
|
105
src/Jackett.Server/Controllers/UIController.cs
Normal file
105
src/Jackett.Server/Controllers/UIController.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jackett.Server.Controllers
|
||||
{
|
||||
[Route("UI/[action]")]
|
||||
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class WebUIController : Controller
|
||||
{
|
||||
private IConfigurationService config;
|
||||
private ServerConfig serverConfig;
|
||||
private ISecuityService securityService;
|
||||
private Logger logger;
|
||||
|
||||
public WebUIController(IConfigurationService config, ISecuityService ss, ServerConfig s, Logger l)
|
||||
{
|
||||
this.config = config;
|
||||
serverConfig = s;
|
||||
securityService = ss;
|
||||
logger = l;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login()
|
||||
{
|
||||
if (string.IsNullOrEmpty(serverConfig.AdminPassword))
|
||||
{
|
||||
await MakeUserAuthenticated();
|
||||
}
|
||||
|
||||
if (User.Identity.IsAuthenticated)
|
||||
{
|
||||
return Redirect("Dashboard");
|
||||
}
|
||||
|
||||
return new PhysicalFileResult(config.GetContentFolder() + "/login.html", "text/html"); ;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return Redirect("Login");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Dashboard([FromForm] string password)
|
||||
{
|
||||
if (password != null && securityService.HashPassword(password) == serverConfig.AdminPassword)
|
||||
{
|
||||
await MakeUserAuthenticated();
|
||||
}
|
||||
|
||||
return Redirect("Dashboard");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Dashboard()
|
||||
{
|
||||
bool logout = HttpContext.Request.Query.Where(x => String.Equals(x.Key, "logout", StringComparison.OrdinalIgnoreCase)
|
||||
&& String.Equals(x.Value, "true", StringComparison.OrdinalIgnoreCase)).Any();
|
||||
|
||||
if (logout)
|
||||
{
|
||||
return Redirect("Logout");
|
||||
}
|
||||
|
||||
return new PhysicalFileResult(config.GetContentFolder() + "/index.html", "text/html");
|
||||
}
|
||||
|
||||
//TODO: Move this to security service once off Mono
|
||||
private async Task MakeUserAuthenticated()
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.Name, "Jackett", ClaimValueTypes.String)
|
||||
};
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(claimsIdentity),
|
||||
new AuthenticationProperties
|
||||
{
|
||||
ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
|
||||
IsPersistent = false,
|
||||
AllowRefresh = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
162
src/Jackett.Server/Helper.cs
Normal file
162
src/Jackett.Server/Helper.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using Autofac;
|
||||
using AutoMapper;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NLog;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Jackett.Server
|
||||
{
|
||||
public static class Helper
|
||||
{
|
||||
public static IContainer ApplicationContainer { get; set; }
|
||||
public static IApplicationLifetime applicationLifetime;
|
||||
private static bool _automapperInitialised = false;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (_automapperInitialised == false)
|
||||
{
|
||||
//Automapper only likes being initialized once per app domain.
|
||||
//Since we can restart Jackett from the command line it's possible that we'll build the container more than once. (tests do this too)
|
||||
InitAutomapper();
|
||||
_automapperInitialised = true;
|
||||
}
|
||||
|
||||
//Load the indexers
|
||||
ServerService.Initalize();
|
||||
|
||||
//Kicks off the update checker
|
||||
ServerService.Start();
|
||||
|
||||
Logger.Debug("Helper initialization complete");
|
||||
}
|
||||
|
||||
public static void RestartWebHost()
|
||||
{
|
||||
Logger.Info("Restart of the web application host (not process) initiated");
|
||||
Program.isWebHostRestart = true;
|
||||
applicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
public static void StopWebHost()
|
||||
{
|
||||
Logger.Info("Jackett is being stopped");
|
||||
applicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
public static IConfigurationService ConfigService
|
||||
{
|
||||
get
|
||||
{
|
||||
return ApplicationContainer.Resolve<IConfigurationService>();
|
||||
}
|
||||
}
|
||||
|
||||
public static IServerService ServerService
|
||||
{
|
||||
get
|
||||
{
|
||||
return ApplicationContainer.Resolve<IServerService>();
|
||||
}
|
||||
}
|
||||
|
||||
public static IServiceConfigService ServiceConfigService
|
||||
{
|
||||
get
|
||||
{
|
||||
return ApplicationContainer.Resolve<IServiceConfigService>();
|
||||
}
|
||||
}
|
||||
|
||||
public static ServerConfig ServerConfiguration
|
||||
{
|
||||
get
|
||||
{
|
||||
return ApplicationContainer.Resolve<ServerConfig>();
|
||||
}
|
||||
}
|
||||
|
||||
public static Logger Logger
|
||||
{
|
||||
get
|
||||
{
|
||||
return ApplicationContainer.Resolve<Logger>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitAutomapper()
|
||||
{
|
||||
Mapper.Initialize(cfg =>
|
||||
{
|
||||
cfg.CreateMap<WebClientByteResult, WebClientStringResult>().ForMember(x => x.Content, opt => opt.Ignore()).AfterMap((be, str) =>
|
||||
{
|
||||
var encoding = be.Request.Encoding ?? Encoding.UTF8;
|
||||
str.Content = encoding.GetString(be.Content);
|
||||
});
|
||||
|
||||
cfg.CreateMap<WebClientStringResult, WebClientByteResult>().ForMember(x => x.Content, opt => opt.Ignore()).AfterMap((str, be) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(str.Content))
|
||||
{
|
||||
var encoding = str.Request.Encoding ?? Encoding.UTF8;
|
||||
be.Content = encoding.GetBytes(str.Content);
|
||||
}
|
||||
});
|
||||
|
||||
cfg.CreateMap<WebClientStringResult, WebClientStringResult>();
|
||||
cfg.CreateMap<WebClientByteResult, WebClientByteResult>();
|
||||
cfg.CreateMap<ReleaseInfo, ReleaseInfo>();
|
||||
|
||||
cfg.CreateMap<ReleaseInfo, TrackerCacheResult>().AfterMap((r, t) =>
|
||||
{
|
||||
if (r.Category != null)
|
||||
{
|
||||
t.CategoryDesc = string.Join(", ", r.Category.Select(x => TorznabCatType.GetCatDesc(x)).Where(x => !string.IsNullOrEmpty(x)));
|
||||
}
|
||||
else
|
||||
{
|
||||
t.CategoryDesc = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static void SetupLogging(ContainerBuilder builder)
|
||||
{
|
||||
Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
if (builder != null)
|
||||
{
|
||||
builder.RegisterInstance(logger).SingleInstance();
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetLogLevel(LogLevel level)
|
||||
{
|
||||
foreach (var rule in LogManager.Configuration.LoggingRules)
|
||||
{
|
||||
if (level == LogLevel.Debug)
|
||||
{
|
||||
if (!rule.Levels.Contains(LogLevel.Debug))
|
||||
{
|
||||
rule.EnableLoggingForLevel(LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rule.Levels.Contains(LogLevel.Debug))
|
||||
{
|
||||
rule.DisableLoggingForLevel(LogLevel.Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
}
|
||||
}
|
313
src/Jackett.Server/HttpWebClientNetCore.cs
Normal file
313
src/Jackett.Server/HttpWebClientNetCore.cs
Normal file
@@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using com.LandonKey.SocksWebProxy;
|
||||
using com.LandonKey.SocksWebProxy.Proxy;
|
||||
using CloudFlareUtilities;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using NLog;
|
||||
using Jackett.Common.Helpers;
|
||||
|
||||
namespace Jackett.Common.Utils.Clients
|
||||
{
|
||||
// custom HttpWebClient based WebClient for netcore (due to changed custom certificate validation API)
|
||||
public class HttpWebClientNetCore : WebClient
|
||||
{
|
||||
static protected Dictionary<string, ICollection<string>> trustedCertificates = new Dictionary<string, ICollection<string>>();
|
||||
static protected string webProxyUrl;
|
||||
static protected IWebProxy webProxy;
|
||||
|
||||
static public void InitProxy(ServerConfig serverConfig)
|
||||
{
|
||||
// dispose old SocksWebProxy
|
||||
if (webProxy != null && webProxy is SocksWebProxy)
|
||||
{
|
||||
((SocksWebProxy)webProxy).Dispose();
|
||||
webProxy = null;
|
||||
}
|
||||
|
||||
webProxyUrl = serverConfig.GetProxyUrl();
|
||||
if (!string.IsNullOrWhiteSpace(webProxyUrl))
|
||||
{
|
||||
if (serverConfig.ProxyType != ProxyType.Http)
|
||||
{
|
||||
var addresses = Dns.GetHostAddressesAsync(serverConfig.ProxyUrl).Result;
|
||||
var socksConfig = new ProxyConfig
|
||||
{
|
||||
SocksAddress = addresses.FirstOrDefault(),
|
||||
Username = serverConfig.ProxyUsername,
|
||||
Password = serverConfig.ProxyPassword,
|
||||
Version = serverConfig.ProxyType == ProxyType.Socks4 ?
|
||||
ProxyConfig.SocksVersion.Four :
|
||||
ProxyConfig.SocksVersion.Five
|
||||
};
|
||||
if (serverConfig.ProxyPort.HasValue)
|
||||
{
|
||||
socksConfig.SocksPort = serverConfig.ProxyPort.Value;
|
||||
}
|
||||
webProxy = new SocksWebProxy(socksConfig, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
NetworkCredential creds = null;
|
||||
if (!serverConfig.ProxyIsAnonymous)
|
||||
{
|
||||
var username = serverConfig.ProxyUsername;
|
||||
var password = serverConfig.ProxyPassword;
|
||||
creds = new NetworkCredential(username, password);
|
||||
}
|
||||
webProxy = new WebProxy(webProxyUrl)
|
||||
{
|
||||
BypassProxyOnLocal = false,
|
||||
Credentials = creds
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HttpWebClientNetCore(IProcessService p, Logger l, IConfigurationService c, ServerConfig sc)
|
||||
: base(p: p,
|
||||
l: l,
|
||||
c: c,
|
||||
sc: sc)
|
||||
{
|
||||
if (webProxyUrl == null)
|
||||
InitProxy(sc);
|
||||
}
|
||||
|
||||
// Called everytime the ServerConfig changes
|
||||
public override void OnNext(ServerConfig value)
|
||||
{
|
||||
var newProxyUrl = serverConfig.GetProxyUrl();
|
||||
if (webProxyUrl != newProxyUrl) // if proxy URL changed
|
||||
InitProxy(serverConfig);
|
||||
}
|
||||
|
||||
override public void Init()
|
||||
{
|
||||
ServicePointManager.DefaultConnectionLimit = 1000;
|
||||
|
||||
if (serverConfig.RuntimeSettings.IgnoreSslErrors == true)
|
||||
{
|
||||
logger.Info(string.Format("HttpWebClient: Disabling certificate validation"));
|
||||
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => { return true; };
|
||||
}
|
||||
}
|
||||
|
||||
override protected async Task<WebClientByteResult> Run(WebRequest webRequest)
|
||||
{
|
||||
ServicePointManager.SecurityProtocol = (SecurityProtocolType)192 | (SecurityProtocolType)768 | (SecurityProtocolType)3072;
|
||||
|
||||
var cookies = new CookieContainer();
|
||||
if (!string.IsNullOrEmpty(webRequest.Cookies))
|
||||
{
|
||||
var uri = new Uri(webRequest.Url);
|
||||
var cookieUrl = new Uri(uri.Scheme + "://" + uri.Host); // don't include the path, Scheme is needed for mono compatibility
|
||||
foreach (var c in webRequest.Cookies.Split(';'))
|
||||
{
|
||||
try
|
||||
{
|
||||
cookies.SetCookies(cookieUrl, c.Trim());
|
||||
}
|
||||
catch (CookieException ex)
|
||||
{
|
||||
logger.Info("(Non-critical) Problem loading cookie {0}, {1}, {2}", uri, c, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (ClearanceHandler clearanceHandlr = new ClearanceHandler())
|
||||
{
|
||||
clearanceHandlr.MaxRetries = 30;
|
||||
using (HttpClientHandler clientHandlr = new HttpClientHandler
|
||||
{
|
||||
CookieContainer = cookies,
|
||||
AllowAutoRedirect = false, // Do not use this - Bugs ahoy! Lost cookies and more.
|
||||
UseCookies = true,
|
||||
Proxy = webProxy,
|
||||
UseProxy = (webProxy != null),
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
|
||||
})
|
||||
{
|
||||
// custom certificate validation handler (netcore version)
|
||||
clientHandlr.ServerCertificateCustomValidationCallback = (request, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
var hash = certificate.GetCertHashString();
|
||||
|
||||
ICollection<string> hosts;
|
||||
|
||||
trustedCertificates.TryGetValue(hash, out hosts);
|
||||
if (hosts != null)
|
||||
{
|
||||
if (hosts.Contains(request.RequestUri.Host))
|
||||
return true;
|
||||
}
|
||||
return sslPolicyErrors == SslPolicyErrors.None;
|
||||
};
|
||||
|
||||
clearanceHandlr.InnerHandler = clientHandlr;
|
||||
using (var client = new HttpClient(clearanceHandlr))
|
||||
{
|
||||
if (webRequest.EmulateBrowser == true)
|
||||
client.DefaultRequestHeaders.Add("User-Agent", BrowserUtil.ChromeUserAgent);
|
||||
else
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "Jackett/" + configService.GetVersion());
|
||||
|
||||
HttpResponseMessage response = null;
|
||||
using (var request = new HttpRequestMessage())
|
||||
{
|
||||
request.Headers.ExpectContinue = false;
|
||||
request.RequestUri = new Uri(webRequest.Url);
|
||||
|
||||
if (webRequest.Headers != null)
|
||||
{
|
||||
foreach (var header in webRequest.Headers)
|
||||
{
|
||||
if (header.Key != "Content-Type")
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(webRequest.Referer))
|
||||
request.Headers.Referrer = new Uri(webRequest.Referer);
|
||||
|
||||
if (!string.IsNullOrEmpty(webRequest.RawBody))
|
||||
{
|
||||
var type = webRequest.Headers.Where(h => h.Key == "Content-Type").Cast<KeyValuePair<string, string>?>().FirstOrDefault();
|
||||
if (type.HasValue)
|
||||
{
|
||||
var str = new StringContent(webRequest.RawBody);
|
||||
str.Headers.Remove("Content-Type");
|
||||
str.Headers.Add("Content-Type", type.Value.Value);
|
||||
request.Content = str;
|
||||
}
|
||||
else
|
||||
request.Content = new StringContent(webRequest.RawBody);
|
||||
request.Method = HttpMethod.Post;
|
||||
}
|
||||
else if (webRequest.Type == RequestType.POST)
|
||||
{
|
||||
if (webRequest.PostData != null)
|
||||
request.Content = new FormUrlEncodedContent(webRequest.PostData);
|
||||
request.Method = HttpMethod.Post;
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Method = HttpMethod.Get;
|
||||
}
|
||||
|
||||
using (response = await client.SendAsync(request))
|
||||
{
|
||||
var result = new WebClientByteResult
|
||||
{
|
||||
Content = await response.Content.ReadAsByteArrayAsync()
|
||||
};
|
||||
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
IEnumerable<string> value = header.Value;
|
||||
result.Headers[header.Key.ToLowerInvariant()] = value.ToArray();
|
||||
}
|
||||
|
||||
// some cloudflare clients are using a refresh header
|
||||
// Pull it out manually
|
||||
if (response.StatusCode == HttpStatusCode.ServiceUnavailable && response.Headers.Contains("Refresh"))
|
||||
{
|
||||
var refreshHeaders = response.Headers.GetValues("Refresh");
|
||||
var redirval = "";
|
||||
var redirtime = 0;
|
||||
if (refreshHeaders != null)
|
||||
{
|
||||
foreach (var value in refreshHeaders)
|
||||
{
|
||||
var start = value.IndexOf("=");
|
||||
var end = value.IndexOf(";");
|
||||
var len = value.Length;
|
||||
if (start > -1)
|
||||
{
|
||||
redirval = value.Substring(start + 1);
|
||||
result.RedirectingTo = redirval;
|
||||
// normally we don't want a serviceunavailable (503) to be a redirect, but that's the nature
|
||||
// of this cloudflare approach..don't want to alter BaseWebResult.IsRedirect because normally
|
||||
// it shoudln't include service unavailable..only if we have this redirect header.
|
||||
response.StatusCode = System.Net.HttpStatusCode.Redirect;
|
||||
redirtime = Int32.Parse(value.Substring(0, end));
|
||||
System.Threading.Thread.Sleep(redirtime * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.Headers.Location != null)
|
||||
{
|
||||
result.RedirectingTo = response.Headers.Location.ToString();
|
||||
}
|
||||
// Mono won't add the baseurl to relative redirects.
|
||||
// e.g. a "Location: /index.php" header will result in the Uri "file:///index.php"
|
||||
// See issue #1200
|
||||
if (result.RedirectingTo != null && result.RedirectingTo.StartsWith("file://"))
|
||||
{
|
||||
// URL decoding apparently is needed to, without it e.g. Demonoid download is broken
|
||||
// TODO: is it always needed (not just for relative redirects)?
|
||||
var newRedirectingTo = WebUtilityHelpers.UrlDecode(result.RedirectingTo, webRequest.Encoding);
|
||||
newRedirectingTo = newRedirectingTo.Replace("file://", request.RequestUri.Scheme + "://" + request.RequestUri.Host);
|
||||
logger.Debug("[MONO relative redirect bug] Rewriting relative redirect URL from " + result.RedirectingTo + " to " + newRedirectingTo);
|
||||
result.RedirectingTo = newRedirectingTo;
|
||||
}
|
||||
result.Status = response.StatusCode;
|
||||
|
||||
// Compatiblity issue between the cookie format and httpclient
|
||||
// Pull it out manually ignoring the expiry date then set it manually
|
||||
// http://stackoverflow.com/questions/14681144/httpclient-not-storing-cookies-in-cookiecontainer
|
||||
IEnumerable<string> cookieHeaders;
|
||||
var responseCookies = new List<Tuple<string, string>>();
|
||||
|
||||
if (response.Headers.TryGetValues("set-cookie", out cookieHeaders))
|
||||
{
|
||||
foreach (var value in cookieHeaders)
|
||||
{
|
||||
var nameSplit = value.IndexOf('=');
|
||||
if (nameSplit > -1)
|
||||
{
|
||||
responseCookies.Add(new Tuple<string, string>(value.Substring(0, nameSplit), value.Substring(0, value.IndexOf(';') == -1 ? value.Length : (value.IndexOf(';'))) + ";"));
|
||||
}
|
||||
}
|
||||
|
||||
var cookieBuilder = new StringBuilder();
|
||||
foreach (var cookieGroup in responseCookies.GroupBy(c => c.Item1))
|
||||
{
|
||||
cookieBuilder.AppendFormat("{0} ", cookieGroup.Last().Item2);
|
||||
}
|
||||
result.Cookies = cookieBuilder.ToString().Trim();
|
||||
}
|
||||
ServerUtil.ResureRedirectIsFullyQualified(webRequest, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public void AddTrustedCertificate(string host, string hash)
|
||||
{
|
||||
hash = hash.ToUpper();
|
||||
ICollection<string> hosts;
|
||||
trustedCertificates.TryGetValue(hash.ToUpper(), out hosts);
|
||||
if (hosts == null)
|
||||
{
|
||||
hosts = new HashSet<string>();
|
||||
trustedCertificates[hash] = hosts;
|
||||
}
|
||||
hosts.Add(host);
|
||||
}
|
||||
}
|
||||
}
|
163
src/Jackett.Server/Initialisation.cs
Normal file
163
src/Jackett.Server/Initialisation.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Jackett.Server.Services;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Jackett.Server
|
||||
{
|
||||
public static class Initialisation
|
||||
{
|
||||
public static void ProcessSettings(RuntimeSettings runtimeSettings, Logger logger)
|
||||
{
|
||||
if (runtimeSettings.ClientOverride != "httpclient" && runtimeSettings.ClientOverride != "httpclient2" && runtimeSettings.ClientOverride != "httpclientnetcore")
|
||||
{
|
||||
logger.Error($"Client override ({runtimeSettings.ClientOverride}) has been deprecated, please remove it from your start arguments");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
if (runtimeSettings.DoSSLFix != null)
|
||||
{
|
||||
logger.Error("SSLFix has been deprecated, please remove it from your start arguments");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
if (runtimeSettings.LogRequests)
|
||||
{
|
||||
logger.Info("Logging enabled.");
|
||||
}
|
||||
|
||||
if (runtimeSettings.TracingEnabled)
|
||||
{
|
||||
logger.Info("Tracing enabled.");
|
||||
}
|
||||
|
||||
if (runtimeSettings.IgnoreSslErrors == true)
|
||||
{
|
||||
logger.Error($"The IgnoreSslErrors option has been deprecated, please remove it from your start arguments");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(runtimeSettings.CustomDataFolder))
|
||||
{
|
||||
logger.Info("Jackett Data will be stored in: " + runtimeSettings.CustomDataFolder);
|
||||
}
|
||||
|
||||
if (runtimeSettings.ProxyConnection != null)
|
||||
{
|
||||
logger.Info("Proxy enabled. " + runtimeSettings.ProxyConnection);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ProcessWindowsSpecificArgs(ConsoleOptions consoleOptions, IProcessService processService, ServerConfig serverConfig, Logger logger)
|
||||
{
|
||||
IServiceConfigService serviceConfigService = new ServiceConfigService();
|
||||
IServerService serverService = new ServerService(null, processService, null, null, logger, null, null, null, serverConfig);
|
||||
|
||||
/* ====== Actions ===== */
|
||||
|
||||
// Install service
|
||||
if (consoleOptions.Install)
|
||||
{
|
||||
logger.Info("Initiating Jackett service install");
|
||||
serviceConfigService.Install();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// Uninstall service
|
||||
if (consoleOptions.Uninstall)
|
||||
{
|
||||
logger.Info("Initiating Jackett service uninstall");
|
||||
serverService.ReserveUrls(false);
|
||||
serviceConfigService.Uninstall();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// Start Service
|
||||
if (consoleOptions.StartService)
|
||||
{
|
||||
if (!serviceConfigService.ServiceRunning())
|
||||
{
|
||||
logger.Info("Initiating Jackett service start");
|
||||
serviceConfigService.Start();
|
||||
}
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// Stop Service
|
||||
if (consoleOptions.StopService)
|
||||
{
|
||||
if (serviceConfigService.ServiceRunning())
|
||||
{
|
||||
logger.Info("Initiating Jackett service stop");
|
||||
serviceConfigService.Stop();
|
||||
}
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
// Reserve urls
|
||||
if (consoleOptions.ReserveUrls)
|
||||
{
|
||||
logger.Info("Initiating ReserveUrls");
|
||||
serverService.ReserveUrls(true);
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ProcessConsoleOverrides(ConsoleOptions consoleOptions, IProcessService processService, ServerConfig serverConfig, IConfigurationService configurationService, Logger logger)
|
||||
{
|
||||
IServerService serverService = new ServerService(null, processService, null, null, logger, null, null, null, serverConfig);
|
||||
|
||||
// Override port
|
||||
if (consoleOptions.Port != 0)
|
||||
{
|
||||
Int32.TryParse(serverConfig.Port.ToString(), out Int32 configPort);
|
||||
|
||||
if (configPort != consoleOptions.Port)
|
||||
{
|
||||
logger.Info("Overriding port to " + consoleOptions.Port);
|
||||
serverConfig.Port = consoleOptions.Port;
|
||||
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
{
|
||||
if (ServerUtil.IsUserAdministrator())
|
||||
{
|
||||
serverService.ReserveUrls(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Unable to switch ports when not running as administrator");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
configurationService.SaveConfig(serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Override listen public
|
||||
if (consoleOptions.ListenPublic || consoleOptions.ListenPrivate)
|
||||
{
|
||||
if (serverConfig.AllowExternal != consoleOptions.ListenPublic)
|
||||
{
|
||||
logger.Info("Overriding external access to " + consoleOptions.ListenPublic);
|
||||
serverConfig.AllowExternal = consoleOptions.ListenPublic;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
if (ServerUtil.IsUserAdministrator())
|
||||
{
|
||||
serverService.ReserveUrls(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Unable to switch to public listening without admin rights.");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
configurationService.SaveConfig(serverConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
src/Jackett.Server/Jackett.Server.csproj
Normal file
59
src/Jackett.Server/Jackett.Server.csproj
Normal file
@@ -0,0 +1,59 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<ApplicationIcon>jackett.ico</ApplicationIcon>
|
||||
<AssemblyName>JackettConsole</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
|
||||
<RuntimeIdentifiers>win-x86;linux-x64;osx-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
|
||||
<RuntimeIdentifiers>win7-x86;linux-x64</RuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Conditionally obtain references for the .NET Core App 2.1 target -->
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.8.1" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.2" />
|
||||
<PackageReference Include="AutoMapper" Version="7.0.1" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="2.1.1" />
|
||||
<PackageReference Include="NLog" Version="4.5.6" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.5.4" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\README.md" Visible="false">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\LICENSE" Visible="false">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\Upstart.config" Visible="false">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
84
src/Jackett.Server/Middleware/CustomExceptionHandler.cs
Normal file
84
src/Jackett.Server/Middleware/CustomExceptionHandler.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jackett.Common;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace Jackett.Server.Middleware
|
||||
{
|
||||
public class CustomExceptionHandler
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private Logger logger;
|
||||
|
||||
public CustomExceptionHandler(RequestDelegate next, Logger l)
|
||||
{
|
||||
_next = next;
|
||||
logger = l;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(httpContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
string msg = "";
|
||||
var json = new JObject();
|
||||
|
||||
logger.Error(ex);
|
||||
|
||||
var message = ex.Message;
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
message += ": " + ex.InnerException.Message;
|
||||
}
|
||||
|
||||
msg = message;
|
||||
|
||||
if (ex is ExceptionWithConfigData)
|
||||
{
|
||||
json["config"] = ((ExceptionWithConfigData)ex).ConfigData.ToJson(null, false);
|
||||
}
|
||||
|
||||
json["result"] = "error";
|
||||
json["error"] = msg;
|
||||
json["stacktrace"] = ex.StackTrace;
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
json["innerstacktrace"] = ex.InnerException.StackTrace;
|
||||
}
|
||||
|
||||
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
await httpContext.Response.WriteAsync(json.ToString());
|
||||
return;
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
logger.Error(ex2, "An exception was thrown attempting to execute the custom exception error handler.");
|
||||
}
|
||||
|
||||
await _next(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension method used to add the middleware to the HTTP request pipeline.
|
||||
public static class CustomExceptionHandlerExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<CustomExceptionHandler>();
|
||||
}
|
||||
}
|
||||
}
|
26
src/Jackett.Server/Middleware/RedirectRules.cs
Normal file
26
src/Jackett.Server/Middleware/RedirectRules.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System;
|
||||
|
||||
namespace Jackett.Server.Middleware
|
||||
{
|
||||
public class RedirectRules
|
||||
{
|
||||
public static void RedirectToDashboard(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
|
||||
if (request.Path == null || string.IsNullOrWhiteSpace(request.Path.ToString()) || request.Path.ToString() == "/"
|
||||
|| request.Path.ToString().Equals("/index.html", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 301 is the status code of permanent redirect
|
||||
var redir = Helper.ServerService.BasePath() + "/UI/Dashboard";
|
||||
var response = context.HttpContext.Response;
|
||||
response.StatusCode = StatusCodes.Status301MovedPermanently;
|
||||
context.Result = RuleResult.EndResponse;
|
||||
response.Headers[HeaderNames.Location] = redir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
185
src/Jackett.Server/Program.cs
Normal file
185
src/Jackett.Server/Program.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NLog;
|
||||
using NLog.Web;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Jackett.Server
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static IConfiguration Configuration { get; set; }
|
||||
private static RuntimeSettings Settings { get; set; }
|
||||
public static bool isWebHostRestart = false;
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
|
||||
|
||||
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
|
||||
var optionsResult = commandLineParser.ParseArguments<ConsoleOptions>(args);
|
||||
var runtimeDictionary = new Dictionary<string, string>();
|
||||
ConsoleOptions consoleOptions = new ConsoleOptions();
|
||||
|
||||
optionsResult.WithNotParsed(errors =>
|
||||
{
|
||||
var text = HelpText.AutoBuild(optionsResult);
|
||||
text.Copyright = " ";
|
||||
text.Heading = "Jackett v" + EnvironmentUtil.JackettVersion;
|
||||
Console.WriteLine(text);
|
||||
Environment.Exit(1);
|
||||
return;
|
||||
});
|
||||
|
||||
optionsResult.WithParsed(options =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(options.Client))
|
||||
{
|
||||
//TODO: Remove libcurl once off owin
|
||||
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("Core", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
if (runningOnDotNetCore)
|
||||
{
|
||||
options.Client = "httpclientnetcore";
|
||||
}
|
||||
else
|
||||
{
|
||||
options.Client = "httpclient";
|
||||
}
|
||||
}
|
||||
|
||||
Settings = options.ToRunTimeSettings();
|
||||
consoleOptions = options;
|
||||
runtimeDictionary = GetValues(Settings);
|
||||
});
|
||||
|
||||
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(Settings);
|
||||
Logger logger = LogManager.GetCurrentClassLogger();
|
||||
logger.Info("Starting Jackett v" + EnvironmentUtil.JackettVersion);
|
||||
|
||||
// create PID file early
|
||||
if (!string.IsNullOrWhiteSpace(Settings.PIDFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var proc = Process.GetCurrentProcess();
|
||||
File.WriteAllText(Settings.PIDFile, proc.Id.ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error while creating the PID file");
|
||||
}
|
||||
}
|
||||
|
||||
Initialisation.ProcessSettings(Settings, logger);
|
||||
|
||||
ISerializeService serializeService = new SerializeService();
|
||||
IProcessService processService = new ProcessService(logger);
|
||||
IConfigurationService configurationService = new ConfigurationService(serializeService, processService, logger, Settings);
|
||||
|
||||
if (consoleOptions.Install || consoleOptions.Uninstall || consoleOptions.StartService || consoleOptions.StopService || consoleOptions.ReserveUrls)
|
||||
{
|
||||
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
|
||||
if (isWindows)
|
||||
{
|
||||
ServerConfig serverConfig = configurationService.BuildServerConfig(Settings);
|
||||
Initialisation.ProcessWindowsSpecificArgs(consoleOptions, processService, serverConfig, logger);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"ReserveUrls and service arguments only apply to Windows, please remove them from your start arguments");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.AddInMemoryCollection(runtimeDictionary);
|
||||
|
||||
Configuration = builder.Build();
|
||||
|
||||
do
|
||||
{
|
||||
if (!isWebHostRestart)
|
||||
{
|
||||
if (consoleOptions.Port != 0 || consoleOptions.ListenPublic || consoleOptions.ListenPrivate)
|
||||
{
|
||||
ServerConfig serverConfiguration = configurationService.BuildServerConfig(Settings);
|
||||
Initialisation.ProcessConsoleOverrides(consoleOptions, processService, serverConfiguration, configurationService, logger);
|
||||
}
|
||||
}
|
||||
|
||||
ServerConfig serverConfig = configurationService.BuildServerConfig(Settings);
|
||||
Int32.TryParse(serverConfig.Port.ToString(), out Int32 configPort);
|
||||
string[] url = serverConfig.GetListenAddresses(serverConfig.AllowExternal).Take(1).ToArray(); //Kestrel doesn't need 127.0.0.1 and localhost to be registered, remove once off OWIN
|
||||
|
||||
isWebHostRestart = false;
|
||||
|
||||
try
|
||||
{
|
||||
logger.Debug("Creating web host...");
|
||||
CreateWebHostBuilder(args, url).Build().Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.InnerException is Microsoft.AspNetCore.Connections.AddressInUseException)
|
||||
{
|
||||
logger.Error("Address already in use: Most likely Jackett is already running. " + ex.Message);
|
||||
Environment.Exit(1);
|
||||
}
|
||||
logger.Error(ex);
|
||||
throw;
|
||||
}
|
||||
} while (isWebHostRestart);
|
||||
}
|
||||
|
||||
public static Dictionary<string, string> GetValues(object obj)
|
||||
{
|
||||
return obj
|
||||
.GetType()
|
||||
.GetProperties()
|
||||
.ToDictionary(p => "RuntimeSettings:" + p.Name, p => p.GetValue(obj) == null ? null : p.GetValue(obj).ToString());
|
||||
}
|
||||
|
||||
private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Settings != null && !string.IsNullOrWhiteSpace(Settings.PIDFile))
|
||||
{
|
||||
var PIDFile = Settings.PIDFile;
|
||||
if (File.Exists(PIDFile))
|
||||
{
|
||||
Console.WriteLine("Deleting PID file " + PIDFile);
|
||||
File.Delete(PIDFile);
|
||||
}
|
||||
LogManager.Shutdown();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.ToString(), "Error while deleting the PID file");
|
||||
}
|
||||
}
|
||||
|
||||
public static IWebHostBuilder CreateWebHostBuilder(string[] args, string[] urls) =>
|
||||
WebHost.CreateDefaultBuilder(args)
|
||||
.UseConfiguration(Configuration)
|
||||
.UseUrls(urls)
|
||||
.PreferHostingUrls(true)
|
||||
.UseStartup<Startup>()
|
||||
.UseNLog();
|
||||
}
|
||||
}
|
211
src/Jackett.Server/Services/ProtectionService.cs
Normal file
211
src/Jackett.Server/Services/ProtectionService.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Jackett.Common;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace Jackett.Server.Services
|
||||
{
|
||||
|
||||
public class ProtectionService : IProtectionService
|
||||
{
|
||||
DataProtectionScope PROTECTION_SCOPE = DataProtectionScope.LocalMachine;
|
||||
private const string JACKETT_KEY = "JACKETT_KEY";
|
||||
const string APPLICATION_KEY = "Dvz66r3n8vhTGip2/quiw5ISyM37f7L2iOdupzdKmzkvXGhAgQiWK+6F+4qpxjPVNks1qO7LdWuVqRlzgLzeW8mChC6JnBMUS1Fin4N2nS9lh4XPuCZ1che75xO92Nk2vyXUo9KSFG1hvEszAuLfG2Mcg1r0sVyVXd2gQDU/TbY=";
|
||||
private byte[] _instanceKey;
|
||||
IDataProtector _protector = null;
|
||||
|
||||
public ProtectionService(ServerConfig config, IDataProtectionProvider provider = null)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
||||
{
|
||||
// We should not be running as root and will only have access to the local store.
|
||||
PROTECTION_SCOPE = DataProtectionScope.CurrentUser;
|
||||
}
|
||||
_instanceKey = Encoding.UTF8.GetBytes(config.InstanceId);
|
||||
|
||||
if (provider != null)
|
||||
{
|
||||
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
|
||||
string purpose = string.IsNullOrEmpty(jackettKey) ? APPLICATION_KEY : jackettKey.ToString();
|
||||
|
||||
_protector = provider.CreateProtector(purpose);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public string Protect(string plainText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plainText))
|
||||
return string.Empty;
|
||||
|
||||
return _protector.Protect(plainText);
|
||||
}
|
||||
|
||||
public string UnProtect(string plainText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plainText))
|
||||
return string.Empty;
|
||||
|
||||
return _protector.Unprotect(plainText);
|
||||
}
|
||||
|
||||
public string LegacyProtect(string plainText)
|
||||
{
|
||||
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
|
||||
|
||||
if (jackettKey == null)
|
||||
{
|
||||
return ProtectDefaultMethod(plainText);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ProtectUsingKey(plainText, jackettKey);
|
||||
}
|
||||
}
|
||||
|
||||
public string LegacyUnProtect(string plainText)
|
||||
{
|
||||
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
|
||||
|
||||
if (jackettKey == null)
|
||||
{
|
||||
return UnProtectDefaultMethod(plainText);
|
||||
}
|
||||
else
|
||||
{
|
||||
return UnProtectUsingKey(plainText, jackettKey);
|
||||
}
|
||||
}
|
||||
|
||||
private string ProtectDefaultMethod(string plainText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plainText))
|
||||
return string.Empty;
|
||||
|
||||
var plainBytes = Encoding.UTF8.GetBytes(plainText);
|
||||
var appKey = Convert.FromBase64String(APPLICATION_KEY);
|
||||
var instanceKey = _instanceKey;
|
||||
var entropy = new byte[appKey.Length + instanceKey.Length];
|
||||
Buffer.BlockCopy(instanceKey, 0, entropy, 0, instanceKey.Length);
|
||||
Buffer.BlockCopy(appKey, 0, entropy, instanceKey.Length, appKey.Length);
|
||||
|
||||
var protectedBytes = ProtectedData.Protect(plainBytes, entropy, PROTECTION_SCOPE);
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
using (RijndaelManaged AES = new RijndaelManaged())
|
||||
{
|
||||
AES.KeySize = 256;
|
||||
AES.BlockSize = 128;
|
||||
|
||||
var key = new Rfc2898DeriveBytes(instanceKey, instanceKey.Reverse().ToArray(), 64);
|
||||
AES.Key = key.GetBytes(AES.KeySize / 8);
|
||||
AES.IV = key.GetBytes(AES.BlockSize / 8);
|
||||
|
||||
AES.Mode = CipherMode.CBC;
|
||||
|
||||
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(protectedBytes, 0, protectedBytes.Length);
|
||||
cs.Close();
|
||||
}
|
||||
protectedBytes = ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(protectedBytes);
|
||||
}
|
||||
|
||||
private string UnProtectDefaultMethod(string plainText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plainText))
|
||||
return string.Empty;
|
||||
|
||||
var protectedBytes = Convert.FromBase64String(plainText);
|
||||
var instanceKey = _instanceKey;
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
using (RijndaelManaged AES = new RijndaelManaged())
|
||||
{
|
||||
AES.KeySize = 256;
|
||||
AES.BlockSize = 128;
|
||||
|
||||
var key = new Rfc2898DeriveBytes(instanceKey, instanceKey.Reverse().ToArray(), 64);
|
||||
AES.Key = key.GetBytes(AES.KeySize / 8);
|
||||
AES.IV = key.GetBytes(AES.BlockSize / 8);
|
||||
|
||||
AES.Mode = CipherMode.CBC;
|
||||
|
||||
using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(protectedBytes, 0, protectedBytes.Length);
|
||||
cs.Close();
|
||||
}
|
||||
protectedBytes = ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
var appKey = Convert.FromBase64String(APPLICATION_KEY);
|
||||
var entropy = new byte[appKey.Length + instanceKey.Length];
|
||||
Buffer.BlockCopy(instanceKey, 0, entropy, 0, instanceKey.Length);
|
||||
Buffer.BlockCopy(appKey, 0, entropy, instanceKey.Length, appKey.Length);
|
||||
|
||||
var unprotectedBytes = ProtectedData.Unprotect(protectedBytes, entropy, PROTECTION_SCOPE);
|
||||
return Encoding.UTF8.GetString(unprotectedBytes);
|
||||
}
|
||||
|
||||
private string ProtectUsingKey(string plainText, string key)
|
||||
{
|
||||
return StringCipher.Encrypt(plainText, key);
|
||||
}
|
||||
|
||||
private string UnProtectUsingKey(string plainText, string key)
|
||||
{
|
||||
return StringCipher.Decrypt(plainText, key);
|
||||
}
|
||||
|
||||
public void Protect<T>(T obj)
|
||||
{
|
||||
var type = obj.GetType();
|
||||
|
||||
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.Public))
|
||||
{
|
||||
if (property.GetCustomAttributes(typeof(JackettProtectedAttribute), false).Count() > 0)
|
||||
{
|
||||
var value = property.GetValue(obj);
|
||||
if (value is string)
|
||||
{
|
||||
var protectedString = Protect(value as string);
|
||||
property.SetValue(obj, protectedString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UnProtect<T>(T obj)
|
||||
{
|
||||
var type = obj.GetType();
|
||||
|
||||
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.Public))
|
||||
{
|
||||
if (property.GetCustomAttributes(typeof(JackettProtectedAttribute), false).Count() > 0)
|
||||
{
|
||||
var value = property.GetValue(obj);
|
||||
if (value is string)
|
||||
{
|
||||
var unprotectedString = UnProtect(value as string);
|
||||
property.SetValue(obj, unprotectedString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
src/Jackett.Server/Services/SecuityService.cs
Normal file
74
src/Jackett.Server/Services/SecuityService.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
|
||||
namespace Jackett.Server.Services
|
||||
{
|
||||
|
||||
class SecuityService : ISecuityService
|
||||
{
|
||||
private const string COOKIENAME = "JACKETT";
|
||||
private ServerConfig _serverConfig;
|
||||
|
||||
public SecuityService(ServerConfig sc)
|
||||
{
|
||||
_serverConfig = sc;
|
||||
}
|
||||
|
||||
public string HashPassword(string input)
|
||||
{
|
||||
if (input == null)
|
||||
return null;
|
||||
// Append key as salt
|
||||
input += _serverConfig.APIKey;
|
||||
|
||||
UnicodeEncoding UE = new UnicodeEncoding();
|
||||
byte[] hashValue;
|
||||
byte[] message = UE.GetBytes(input);
|
||||
|
||||
SHA512Managed hashString = new SHA512Managed();
|
||||
string hex = "";
|
||||
|
||||
hashValue = hashString.ComputeHash(message);
|
||||
foreach (byte x in hashValue)
|
||||
{
|
||||
hex += String.Format("{0:x2}", x);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
public void Login(HttpResponseMessage response)
|
||||
{
|
||||
// Login
|
||||
response.Headers.Add("Set-Cookie", COOKIENAME + "=" + _serverConfig.AdminPassword + "; path=/");
|
||||
}
|
||||
|
||||
public void Logout(HttpResponseMessage response)
|
||||
{
|
||||
// Logout
|
||||
response.Headers.Add("Set-Cookie", COOKIENAME + "=; path=/");
|
||||
}
|
||||
|
||||
public bool CheckAuthorised(HttpRequestMessage request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_serverConfig.AdminPassword))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
var cookie = request.Headers.GetValues(COOKIENAME).FirstOrDefault();
|
||||
if (cookie != null)
|
||||
{
|
||||
return cookie == _serverConfig.AdminPassword;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
341
src/Jackett.Server/Services/ServerService.cs
Normal file
341
src/Jackett.Server/Services/ServerService.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
namespace Jackett.Server.Services
|
||||
{
|
||||
public class ServerService : IServerService
|
||||
{
|
||||
private IIndexerManagerService indexerService;
|
||||
private IProcessService processService;
|
||||
private ISerializeService serializeService;
|
||||
private IConfigurationService configService;
|
||||
private Logger logger;
|
||||
private Common.Utils.Clients.WebClient client;
|
||||
private IUpdateService updater;
|
||||
private List<string> _notices = new List<string>();
|
||||
private ServerConfig config;
|
||||
private IProtectionService _protectionService;
|
||||
|
||||
public ServerService(IIndexerManagerService i, IProcessService p, ISerializeService s, IConfigurationService c, Logger l, Common.Utils.Clients.WebClient w, IUpdateService u, IProtectionService protectionService, ServerConfig serverConfig)
|
||||
{
|
||||
indexerService = i;
|
||||
processService = p;
|
||||
serializeService = s;
|
||||
configService = c;
|
||||
logger = l;
|
||||
client = w;
|
||||
updater = u;
|
||||
config = serverConfig;
|
||||
_protectionService = protectionService;
|
||||
}
|
||||
|
||||
public List<string> notices
|
||||
{
|
||||
get
|
||||
{
|
||||
return _notices;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri ConvertToProxyLink(Uri link, string serverUrl, string indexerId, string action = "dl", string file = "t")
|
||||
{
|
||||
if (link == null || (link.IsAbsoluteUri && link.Scheme == "magnet" && action != "bh")) // no need to convert a magnet link to a proxy link unless it's a blackhole link
|
||||
return link;
|
||||
|
||||
var encryptedLink = _protectionService.Protect(link.ToString());
|
||||
var encodedLink = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(encryptedLink));
|
||||
string urlEncodedFile = WebUtility.UrlEncode(file);
|
||||
var proxyLink = string.Format("{0}{1}/{2}/?jackett_apikey={3}&path={4}&file={5}", serverUrl, action, indexerId, config.APIKey, encodedLink, urlEncodedFile);
|
||||
return new Uri(proxyLink);
|
||||
}
|
||||
|
||||
public string BasePath()
|
||||
{
|
||||
if (config.BasePathOverride == null || config.BasePathOverride == "")
|
||||
{
|
||||
return "";
|
||||
}
|
||||
var path = config.BasePathOverride;
|
||||
if (path.EndsWith("/"))
|
||||
{
|
||||
path = path.TrimEnd('/');
|
||||
}
|
||||
if (!path.StartsWith("/"))
|
||||
{
|
||||
path = "/" + path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public void Initalize()
|
||||
{
|
||||
try
|
||||
{
|
||||
var x = Environment.OSVersion;
|
||||
var runtimedir = RuntimeEnvironment.GetRuntimeDirectory();
|
||||
logger.Info("Environment version: " + Environment.Version.ToString() + " (" + runtimedir + ")");
|
||||
logger.Info("OS version: " + Environment.OSVersion.ToString() + (Environment.Is64BitOperatingSystem ? " (64bit OS)" : "") + (Environment.Is64BitProcess ? " (64bit process)" : ""));
|
||||
|
||||
try
|
||||
{
|
||||
ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
|
||||
logger.Info("ThreadPool MaxThreads: " + workerThreads + " workerThreads, " + completionPortThreads + " completionPortThreads");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Error while getting MaxThreads details: " + e);
|
||||
}
|
||||
|
||||
logger.Info("App config/log directory: " + configService.GetAppDataFolder());
|
||||
|
||||
try
|
||||
{
|
||||
var issuefile = "/etc/issue";
|
||||
if (File.Exists(issuefile))
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(issuefile))
|
||||
{
|
||||
string firstLine = reader.ReadLine();
|
||||
if (firstLine != null)
|
||||
logger.Info("issue: " + firstLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error while reading the issue file");
|
||||
}
|
||||
|
||||
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("Core", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
|
||||
Type monotype = Type.GetType("Mono.Runtime");
|
||||
if (monotype != null && !runningOnDotNetCore)
|
||||
{
|
||||
MethodInfo displayName = monotype.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var monoVersion = "unknown";
|
||||
if (displayName != null)
|
||||
monoVersion = displayName.Invoke(null, null).ToString();
|
||||
logger.Info("mono version: " + monoVersion);
|
||||
|
||||
var monoVersionO = new Version(monoVersion.Split(' ')[0]);
|
||||
|
||||
if (monoVersionO.Major < 5 || (monoVersionO.Major == 5 && monoVersionO.Minor < 8))
|
||||
{
|
||||
//Hard minimum of 5.8
|
||||
//5.4 throws a SIGABRT, looks related to this which was fixed in 5.8 https://bugzilla.xamarin.com/show_bug.cgi?id=60625
|
||||
|
||||
logger.Error("Your mono version is too old. Please update to the latest version from http://www.mono-project.com/download/");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
|
||||
if (monoVersionO.Major < 5 || (monoVersionO.Major == 5 && monoVersionO.Minor < 8))
|
||||
{
|
||||
string notice = "A minimum Mono version of 5.8 is required. Please update to the latest version from http://www.mono-project.com/download/";
|
||||
_notices.Add(notice);
|
||||
logger.Error(notice);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check for mono-devel
|
||||
// Is there any better way which doesn't involve a hard cashes?
|
||||
var mono_devel_file = Path.Combine(runtimedir, "mono-api-info.exe");
|
||||
if (!File.Exists(mono_devel_file))
|
||||
{
|
||||
var notice = "It looks like the mono-devel package is not installed, please make sure it's installed to avoid crashes.";
|
||||
_notices.Add(notice);
|
||||
logger.Error(notice);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error while checking for mono-devel");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check for ca-certificates-mono
|
||||
var mono_cert_file = Path.Combine(runtimedir, "cert-sync.exe");
|
||||
if (!File.Exists(mono_cert_file))
|
||||
{
|
||||
var notice = "The ca-certificates-mono package is not installed, HTTPS trackers won't work. Please install it.";
|
||||
_notices.Add(notice);
|
||||
logger.Error(notice);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error while checking for ca-certificates-mono");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Encoding.GetEncoding("windows-1255");
|
||||
}
|
||||
catch (NotSupportedException e)
|
||||
{
|
||||
logger.Debug(e);
|
||||
logger.Error(e.Message + " Most likely the mono-locale-extras package is not installed.");
|
||||
Environment.Exit(2);
|
||||
}
|
||||
|
||||
// check if the certificate store was initialized using Mono.Security.X509.X509StoreManager.TrustedRootCertificates.Count
|
||||
try
|
||||
{
|
||||
var monoSecurity = Assembly.Load("Mono.Security");
|
||||
Type monoX509StoreManager = monoSecurity.GetType("Mono.Security.X509.X509StoreManager");
|
||||
if (monoX509StoreManager != null)
|
||||
{
|
||||
var TrustedRootCertificatesProperty = monoX509StoreManager.GetProperty("TrustedRootCertificates");
|
||||
var TrustedRootCertificates = (ICollection)TrustedRootCertificatesProperty.GetValue(null);
|
||||
|
||||
logger.Info("TrustedRootCertificates count: " + TrustedRootCertificates.Count);
|
||||
|
||||
if (TrustedRootCertificates.Count == 0)
|
||||
{
|
||||
var CACertificatesFiles = new string[] {
|
||||
"/etc/ssl/certs/ca-certificates.crt", // Debian based
|
||||
"/etc/pki/tls/certs/ca-bundle.c", // RedHat based
|
||||
"/etc/ssl/ca-bundle.pem", // SUSE
|
||||
};
|
||||
|
||||
var notice = "The mono certificate store is not initialized.<br/>\n";
|
||||
var logSpacer = " ";
|
||||
var CACertificatesFile = CACertificatesFiles.Where(f => File.Exists(f)).FirstOrDefault();
|
||||
var CommandRoot = "curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync /dev/stdin";
|
||||
var CommandUser = "curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync --user /dev/stdin";
|
||||
if (CACertificatesFile != null)
|
||||
{
|
||||
CommandRoot = "cert-sync " + CACertificatesFile;
|
||||
CommandUser = "cert-sync --user " + CACertificatesFile;
|
||||
}
|
||||
notice += logSpacer + "Please run the following command as root:<br/>\n";
|
||||
notice += logSpacer + "<pre>" + CommandRoot + "</pre><br/>\n";
|
||||
notice += logSpacer + "If you don't have root access or you're running MacOS, please run the following command as the jackett user (" + Environment.UserName + "):<br/>\n";
|
||||
notice += logSpacer + "<pre>" + CommandUser + "</pre>";
|
||||
_notices.Add(notice);
|
||||
logger.Error(Regex.Replace(notice, "<.*?>", String.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error while chekcing the mono certificate store");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Error while getting environment details: " + e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Environment.UserName == "root")
|
||||
{
|
||||
var notice = "Jackett is running with root privileges. You should run Jackett as an unprivileged user.";
|
||||
_notices.Add(notice);
|
||||
logger.Error(notice);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e, "Error while checking the username");
|
||||
}
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
||||
// Load indexers
|
||||
indexerService.InitIndexers(configService.GetCardigannDefinitionsFolders());
|
||||
client.Init();
|
||||
updater.CleanupTempDir();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
updater.StartUpdateChecker();
|
||||
}
|
||||
|
||||
public void ReserveUrls(bool doInstall = true)
|
||||
{
|
||||
logger.Debug("Unreserving Urls");
|
||||
config.GetListenAddresses(false).ToList().ForEach(u => RunNetSh(string.Format("http delete urlacl {0}", u)));
|
||||
config.GetListenAddresses(true).ToList().ForEach(u => RunNetSh(string.Format("http delete urlacl {0}", u)));
|
||||
if (doInstall)
|
||||
{
|
||||
logger.Debug("Reserving Urls");
|
||||
config.GetListenAddresses(true).ToList().ForEach(u => RunNetSh(string.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", u)));
|
||||
logger.Debug("Urls reserved");
|
||||
}
|
||||
}
|
||||
|
||||
private void RunNetSh(string args)
|
||||
{
|
||||
processService.StartProcessAndLog("netsh.exe", args);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
// Only needed for Owin
|
||||
}
|
||||
|
||||
public string GetServerUrl(Object obj)
|
||||
{
|
||||
string serverUrl = "";
|
||||
|
||||
if (obj is HttpRequest request)
|
||||
{
|
||||
var scheme = request.Scheme;
|
||||
var port = request.HttpContext.Request.Host.Port;
|
||||
|
||||
// Check for protocol headers added by reverse proxys
|
||||
// X-Forwarded-Proto: A de facto standard for identifying the originating protocol of an HTTP request
|
||||
var X_Forwarded_Proto = request.Headers.Where(x => x.Key == "X-Forwarded-Proto").Select(x => x.Value).FirstOrDefault();
|
||||
if (X_Forwarded_Proto.Count > 0)
|
||||
{
|
||||
scheme = X_Forwarded_Proto.First();
|
||||
}
|
||||
// Front-End-Https: Non-standard header field used by Microsoft applications and load-balancers
|
||||
else if (request.Headers.Where(x => x.Key == "Front-End-Https" && x.Value.FirstOrDefault() == "on").Any())
|
||||
{
|
||||
scheme = "https";
|
||||
}
|
||||
|
||||
//default to 443 if the Host header doesn't contain the port (needed for reverse proxy setups)
|
||||
if (scheme == "https" && !request.HttpContext.Request.Host.Value.Contains(":"))
|
||||
{
|
||||
port = 443;
|
||||
}
|
||||
|
||||
serverUrl = string.Format("{0}://{1}:{2}{3}/", scheme, request.HttpContext.Request.Host.Host, port, BasePath());
|
||||
}
|
||||
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
public string GetBlackholeDirectory()
|
||||
{
|
||||
return config.BlackholeDir;
|
||||
}
|
||||
|
||||
public string GetApiKey()
|
||||
{
|
||||
return config.APIKey;
|
||||
}
|
||||
}
|
||||
}
|
121
src/Jackett.Server/Services/ServiceConfigService.cs
Normal file
121
src/Jackett.Server/Services/ServiceConfigService.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.ServiceProcess;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using System.Reflection;
|
||||
using Jackett.Common.Services;
|
||||
|
||||
namespace Jackett.Server.Services
|
||||
{
|
||||
public class ServiceConfigService : IServiceConfigService
|
||||
{
|
||||
private const string NAME = "Jackett";
|
||||
private const string DESCRIPTION = "API Support for your favorite torrent trackers";
|
||||
private const string SERVICEEXE = "JackettService.exe";
|
||||
|
||||
private IProcessService processService;
|
||||
private Logger logger;
|
||||
|
||||
public ServiceConfigService()
|
||||
{
|
||||
logger = LogManager.GetCurrentClassLogger();
|
||||
processService = new ProcessService(logger);
|
||||
}
|
||||
|
||||
public bool ServiceExists()
|
||||
{
|
||||
return GetService(NAME) != null;
|
||||
}
|
||||
|
||||
public bool ServiceRunning()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
if (service == null)
|
||||
return false;
|
||||
return service.Status == ServiceControllerStatus.Running;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
|
||||
var service = GetService(NAME);
|
||||
service.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
service.Stop();
|
||||
}
|
||||
|
||||
public ServiceController GetService(string serviceName)
|
||||
{
|
||||
return ServiceController.GetServices().FirstOrDefault(c => String.Equals(c.ServiceName, serviceName, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public void Install()
|
||||
{
|
||||
if (ServiceExists())
|
||||
{
|
||||
logger.Warn("The service is already installed!");
|
||||
}
|
||||
else
|
||||
{
|
||||
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
|
||||
|
||||
var exePath = Path.Combine(applicationFolder, SERVICEEXE);
|
||||
if (!File.Exists(exePath) && Debugger.IsAttached)
|
||||
{
|
||||
exePath = Path.Combine(applicationFolder, "..\\..\\..\\Jackett.Service\\bin\\Debug", SERVICEEXE);
|
||||
}
|
||||
|
||||
string arg = $"create {NAME} start= auto binpath= \"{exePath}\" DisplayName= {NAME}";
|
||||
|
||||
processService.StartProcessAndLog("sc.exe", arg, true);
|
||||
|
||||
processService.StartProcessAndLog("sc.exe", $"description {NAME} \"{DESCRIPTION}\"", true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
RemoveService();
|
||||
|
||||
processService.StartProcessAndLog("sc.exe", $"delete {NAME}", true);
|
||||
|
||||
logger.Info("The service was uninstalled.");
|
||||
}
|
||||
|
||||
public void RemoveService()
|
||||
{
|
||||
var service = GetService(NAME);
|
||||
if(service == null)
|
||||
{
|
||||
logger.Warn("The service is already uninstalled");
|
||||
return;
|
||||
}
|
||||
if (service.Status != ServiceControllerStatus.Stopped)
|
||||
{
|
||||
service.Stop();
|
||||
service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60));
|
||||
|
||||
service.Refresh();
|
||||
if (service.Status == ServiceControllerStatus.Stopped)
|
||||
{
|
||||
logger.Info("Service stopped.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to stop the service");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("The service was already stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
148
src/Jackett.Server/Startup.cs
Normal file
148
src/Jackett.Server/Startup.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Plumbing;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using Jackett.Server.Middleware;
|
||||
using Jackett.Server.Services;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Jackett.Server
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public IServiceProvider ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddResponseCompression();
|
||||
|
||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
options =>
|
||||
{
|
||||
options.LoginPath = new PathString("/UI/Login");
|
||||
options.AccessDeniedPath = new PathString("/UI/Login");
|
||||
options.LogoutPath = new PathString("/UI/Logout");
|
||||
options.Cookie.Name = "Jackett";
|
||||
});
|
||||
|
||||
services.AddMvc(config =>
|
||||
{
|
||||
var policy = new AuthorizationPolicyBuilder()
|
||||
.RequireAuthenticatedUser()
|
||||
.Build();
|
||||
config.Filters.Add(new AuthorizeFilter(policy));
|
||||
})
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.SerializerSettings.ContractResolver = new DefaultContractResolver(); //Web app uses Pascal Case JSON
|
||||
})
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
|
||||
|
||||
RuntimeSettings runtimeSettings = new RuntimeSettings();
|
||||
Configuration.GetSection("RuntimeSettings").Bind(runtimeSettings);
|
||||
|
||||
DirectoryInfo dataProtectionFolder = new DirectoryInfo(Path.Combine(runtimeSettings.DataFolder, "DataProtection"));
|
||||
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(dataProtectionFolder)
|
||||
.SetApplicationName("Jackett");
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
|
||||
Helper.SetupLogging(builder);
|
||||
|
||||
builder.Populate(services);
|
||||
builder.RegisterModule(new JackettModule(runtimeSettings));
|
||||
builder.RegisterType<SecuityService>().As<ISecuityService>();
|
||||
builder.RegisterType<ServerService>().As<IServerService>();
|
||||
builder.RegisterType<ProtectionService>().As<IProtectionService>();
|
||||
builder.RegisterType<ServiceConfigService>().As<IServiceConfigService>();
|
||||
if (runtimeSettings.ClientOverride == "httpclientnetcore")
|
||||
builder.RegisterType<HttpWebClientNetCore>().As<WebClient>();
|
||||
|
||||
IContainer container = builder.Build();
|
||||
Helper.ApplicationContainer = container;
|
||||
|
||||
Helper.Logger.Debug("Autofac container built");
|
||||
|
||||
Helper.Initialize();
|
||||
|
||||
return new AutofacServiceProvider(container);
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
|
||||
{
|
||||
applicationLifetime.ApplicationStopping.Register(OnShutdown);
|
||||
Helper.applicationLifetime = applicationLifetime;
|
||||
app.UseResponseCompression();
|
||||
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
app.UseCustomExceptionHandler();
|
||||
|
||||
string serverBasePath = Helper.ServerService.BasePath() ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(serverBasePath))
|
||||
{
|
||||
app.UsePathBase(serverBasePath);
|
||||
}
|
||||
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
|
||||
var rewriteOptions = new RewriteOptions()
|
||||
.AddRewrite(@"^torznab\/([\w-]*)", "api/v2.0/indexers/$1/results/torznab", skipRemainingRules: true) //legacy torznab route
|
||||
.AddRewrite(@"^potato\/([\w-]*)", "api/v2.0/indexers/$1/results/potato", skipRemainingRules: true) //legacy potato route
|
||||
.Add(RedirectRules.RedirectToDashboard);
|
||||
|
||||
app.UseRewriter(rewriteOptions);
|
||||
|
||||
app.UseFileServer(new FileServerOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Helper.ConfigService.GetContentFolder()),
|
||||
RequestPath = "",
|
||||
EnableDefaultFiles = true,
|
||||
EnableDirectoryBrowsing = false
|
||||
});
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseMvc();
|
||||
}
|
||||
|
||||
private void OnShutdown()
|
||||
{
|
||||
//this code is called when the application stops
|
||||
}
|
||||
}
|
||||
}
|
10
src/Jackett.Server/appsettings.Development.json
Normal file
10
src/Jackett.Server/appsettings.Development.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
15
src/Jackett.Server/appsettings.json
Normal file
15
src/Jackett.Server/appsettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"Debug": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/Jackett.Server/jackett.ico
Normal file
BIN
src/Jackett.Server/jackett.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 KiB |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
|
||||
</startup>
|
||||
<system.net>
|
||||
<settings>
|
||||
<!-- needed to make the broken incapsula DDoS protection work on windows(e.g. for KickAssTorrent), see https://social.technet.microsoft.com/Forums/de-DE/b10b16d1-8eea-4b52-8aeb-f96ea87135fa/sectionresponseheader-detailcr-must-be-followed-by-lf?forum=powerquery -->
|
||||
<httpWebRequest useUnsafeHeaderParsing="true" />
|
||||
</settings>
|
||||
</system.net>
|
||||
</configuration>
|
@@ -9,11 +9,12 @@
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Jackett.Service</RootNamespace>
|
||||
<AssemblyName>JackettService</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
<RuntimeIdentifier>win</RuntimeIdentifier>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -65,25 +66,14 @@
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="jackett.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CurlSharp\CurlSharp.csproj">
|
||||
<Project>{74420a79-cc16-442c-8b1e-7c1b913844f0}</Project>
|
||||
<Name>CurlSharp</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj">
|
||||
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
|
||||
<Name>Jackett.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett\Jackett.csproj">
|
||||
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
|
||||
<Name>Jackett</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
@@ -1,29 +1,102 @@
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.ServiceProcess;
|
||||
using Jackett.Common;
|
||||
|
||||
namespace Jackett.Service
|
||||
{
|
||||
public partial class Service : ServiceBase
|
||||
{
|
||||
private IProcessService processService;
|
||||
private Process consoleProcess;
|
||||
private Logger logger;
|
||||
private bool serviceStopInitiated;
|
||||
|
||||
public Service()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
RuntimeSettings runtimeSettings = new RuntimeSettings()
|
||||
{
|
||||
CustomLogFileName = "ServiceLog.txt"
|
||||
};
|
||||
|
||||
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
|
||||
logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
logger.Info("Initiating Jackett Service v" + EnvironmentUtil.JackettVersion);
|
||||
|
||||
processService = new ProcessService(logger);
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
Engine.BuildContainer(new RuntimeSettings(), new WebApi2Module());
|
||||
Engine.Logger.Info("Service starting");
|
||||
Engine.Server.Initalize();
|
||||
Engine.Server.Start();
|
||||
Engine.Logger.Info("Service started");
|
||||
logger.Info("Service starting");
|
||||
serviceStopInitiated = false;
|
||||
StartConsoleApplication();
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
Engine.Logger.Info("Service stopping");
|
||||
Engine.Server.Stop();
|
||||
logger.Info("Service stopping");
|
||||
serviceStopInitiated = true;
|
||||
StopConsoleApplication();
|
||||
}
|
||||
|
||||
private void StartConsoleApplication()
|
||||
{
|
||||
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
|
||||
|
||||
var exePath = Path.Combine(applicationFolder, "JackettConsole.exe");
|
||||
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = exePath,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
consoleProcess = Process.Start(startInfo);
|
||||
consoleProcess.EnableRaisingEvents = true;
|
||||
consoleProcess.Exited += ProcessExited;
|
||||
consoleProcess.ErrorDataReceived += ProcessErrorDataReceived;
|
||||
}
|
||||
|
||||
private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
logger.Error(e.Data);
|
||||
}
|
||||
|
||||
private void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
logger.Info("Console process exited");
|
||||
|
||||
if (!serviceStopInitiated)
|
||||
{
|
||||
logger.Info("Service stop not responsible for process exit");
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void StopConsoleApplication()
|
||||
{
|
||||
if (consoleProcess != null && !consoleProcess.HasExited)
|
||||
{
|
||||
consoleProcess.StandardInput.Close();
|
||||
consoleProcess.WaitForExit(2000);
|
||||
if (consoleProcess != null && !consoleProcess.HasExited)
|
||||
{
|
||||
consoleProcess.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,37 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net452</TargetFramework>
|
||||
<TargetFramework>net461</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Indexers\**" />
|
||||
<EmbeddedResource Remove="Indexers\**" />
|
||||
<None Remove="Indexers\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Util\Invalid-RSS.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Util\Invalid-RSS.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.8.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="5.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
<PackageReference Include="NUnit" Version="3.10.1" />
|
||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
|
||||
<ProjectReference Include="..\Jackett\Jackett.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@@ -18,7 +18,7 @@ namespace Jackett.Test
|
||||
{
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterModule(new JackettModule(new RuntimeSettings()));
|
||||
builder.RegisterModule<WebApi2Module>();
|
||||
builder.RegisterType<Jackett.Services.ProtectionService>().As<IProtectionService>();
|
||||
builder.RegisterType<TestWebClient>().As<WebClient>().SingleInstance();
|
||||
builder.RegisterInstance(LogManager.GetCurrentClassLogger()).SingleInstance();
|
||||
builder.RegisterType<TestIndexerManagerServiceHelper>().As<IIndexerManagerService>().SingleInstance();
|
||||
|
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
|
||||
</startup>
|
||||
|
||||
</configuration>
|
@@ -9,11 +9,12 @@
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Jackett.Tray</RootNamespace>
|
||||
<AssemblyName>JackettTray</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
<RuntimeIdentifier>win</RuntimeIdentifier>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -65,6 +66,7 @@
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="TrayConsoleOptions.cs" />
|
||||
<EmbeddedResource Include="Main.resx">
|
||||
<DependentUpon>Main.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
@@ -88,11 +90,6 @@
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="jackett.ico" />
|
||||
</ItemGroup>
|
||||
@@ -101,10 +98,6 @@
|
||||
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
|
||||
<Name>Jackett.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett\Jackett.csproj">
|
||||
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
|
||||
<Name>Jackett</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<COMReference Include="IWshRuntimeLibrary">
|
||||
@@ -117,6 +110,11 @@
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</COMReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser">
|
||||
<Version>2.2.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
18
src/Jackett.Tray/Main.Designer.cs
generated
18
src/Jackett.Tray/Main.Designer.cs
generated
@@ -60,51 +60,51 @@
|
||||
this.toolStripMenuItemAutoStart,
|
||||
this.toolStripMenuItemShutdown});
|
||||
this.contextMenuStrip1.Name = "contextMenuStrip1";
|
||||
this.contextMenuStrip1.Size = new System.Drawing.Size(292, 148);
|
||||
this.contextMenuStrip1.Size = new System.Drawing.Size(296, 126);
|
||||
this.contextMenuStrip1.Opening += new System.ComponentModel.CancelEventHandler(this.contextMenuStrip1_Opening);
|
||||
//
|
||||
// toolStripMenuItemWebUI
|
||||
//
|
||||
this.toolStripMenuItemWebUI.Name = "toolStripMenuItemWebUI";
|
||||
this.toolStripMenuItemWebUI.Size = new System.Drawing.Size(291, 22);
|
||||
this.toolStripMenuItemWebUI.Size = new System.Drawing.Size(295, 22);
|
||||
this.toolStripMenuItemWebUI.Text = "Open Web UI";
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(288, 6);
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(292, 6);
|
||||
//
|
||||
// backgroundMenuItem
|
||||
//
|
||||
this.backgroundMenuItem.Enabled = false;
|
||||
this.backgroundMenuItem.Name = "backgroundMenuItem";
|
||||
this.backgroundMenuItem.Size = new System.Drawing.Size(291, 22);
|
||||
this.backgroundMenuItem.Text = "Jacket is running as a background service";
|
||||
this.backgroundMenuItem.Size = new System.Drawing.Size(295, 22);
|
||||
this.backgroundMenuItem.Text = "Jackett is running as a background service";
|
||||
//
|
||||
// serviceControlMenuItem
|
||||
//
|
||||
this.serviceControlMenuItem.Name = "serviceControlMenuItem";
|
||||
this.serviceControlMenuItem.Size = new System.Drawing.Size(291, 22);
|
||||
this.serviceControlMenuItem.Size = new System.Drawing.Size(295, 22);
|
||||
this.serviceControlMenuItem.Text = "Start Service";
|
||||
this.serviceControlMenuItem.Click += new System.EventHandler(this.serviceControlMenuItem_Click);
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
this.toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
this.toolStripSeparator2.Size = new System.Drawing.Size(288, 6);
|
||||
this.toolStripSeparator2.Size = new System.Drawing.Size(292, 6);
|
||||
//
|
||||
// toolStripMenuItemAutoStart
|
||||
//
|
||||
this.toolStripMenuItemAutoStart.CheckOnClick = true;
|
||||
this.toolStripMenuItemAutoStart.Name = "toolStripMenuItemAutoStart";
|
||||
this.toolStripMenuItemAutoStart.Size = new System.Drawing.Size(291, 22);
|
||||
this.toolStripMenuItemAutoStart.Size = new System.Drawing.Size(295, 22);
|
||||
this.toolStripMenuItemAutoStart.Text = "Auto-start on boot";
|
||||
this.toolStripMenuItemAutoStart.Visible = false;
|
||||
//
|
||||
// toolStripMenuItemShutdown
|
||||
//
|
||||
this.toolStripMenuItemShutdown.Name = "toolStripMenuItemShutdown";
|
||||
this.toolStripMenuItemShutdown.Size = new System.Drawing.Size(291, 22);
|
||||
this.toolStripMenuItemShutdown.Size = new System.Drawing.Size(295, 22);
|
||||
this.toolStripMenuItemShutdown.Text = "Shutdown";
|
||||
//
|
||||
// Main
|
||||
|
@@ -1,32 +1,52 @@
|
||||
using System;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Jackett.Common;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Utils;
|
||||
using Microsoft.Win32;
|
||||
using Jackett;
|
||||
using Jackett.Utils;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
|
||||
namespace Jackett.Tray
|
||||
{
|
||||
public partial class Main : Form
|
||||
{
|
||||
public Main()
|
||||
private IProcessService processService;
|
||||
private IServiceConfigService windowsService;
|
||||
private ITrayLockService trayLockService;
|
||||
private ISerializeService serializeService;
|
||||
private IConfigurationService configurationService;
|
||||
private ServerConfig serverConfig;
|
||||
private Process consoleProcess;
|
||||
private Logger logger;
|
||||
private bool closeApplicationInitiated;
|
||||
|
||||
public Main(string updatedVersion)
|
||||
{
|
||||
Hide();
|
||||
InitializeComponent();
|
||||
|
||||
RuntimeSettings runtimeSettings = new RuntimeSettings()
|
||||
{
|
||||
CustomLogFileName = "TrayLog.txt"
|
||||
};
|
||||
|
||||
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
|
||||
logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
logger.Info("Starting Jackett Tray v" + EnvironmentUtil.JackettVersion);
|
||||
|
||||
processService = new ProcessService(logger);
|
||||
windowsService = new WindowsServiceConfigService(processService, logger);
|
||||
trayLockService = new TrayLockService();
|
||||
serializeService = new SerializeService();
|
||||
configurationService = new ConfigurationService(serializeService, processService, logger, runtimeSettings);
|
||||
serverConfig = configurationService.BuildServerConfig(runtimeSettings);
|
||||
|
||||
toolStripMenuItemAutoStart.Checked = AutoStart;
|
||||
toolStripMenuItemAutoStart.CheckedChanged += toolStripMenuItemAutoStart_CheckedChanged;
|
||||
|
||||
@@ -34,18 +54,26 @@ namespace Jackett.Tray
|
||||
toolStripMenuItemShutdown.Click += toolStripMenuItemShutdown_Click;
|
||||
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
{
|
||||
toolStripMenuItemAutoStart.Visible = true;
|
||||
}
|
||||
|
||||
Engine.BuildContainer(new RuntimeSettings(),new WebApi2Module());
|
||||
Engine.Server.Initalize();
|
||||
|
||||
if (!Engine.ServiceConfig.ServiceExists())
|
||||
if (!windowsService.ServiceExists())
|
||||
{
|
||||
// We are not installed as a service so just the web server too and run from the tray.
|
||||
Engine.Logger.Info("Starting server from tray");
|
||||
Engine.Server.Start();
|
||||
// We are not installed as a service so just start the web server via JackettConsole and run from the tray.
|
||||
logger.Info("Starting server from tray");
|
||||
StartConsoleApplication();
|
||||
}
|
||||
|
||||
updatedVersion = updatedVersion.Equals("yes", StringComparison.OrdinalIgnoreCase) ? EnvironmentUtil.JackettVersion : updatedVersion;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(updatedVersion))
|
||||
{
|
||||
notifyIcon1.BalloonTipTitle = "Jackett";
|
||||
notifyIcon1.BalloonTipText = $"Jackett has updated to version {updatedVersion}";
|
||||
notifyIcon1.BalloonTipIcon = ToolTipIcon.Info;
|
||||
notifyIcon1.ShowBalloonTip(10000);
|
||||
logger.Info($"Display balloon tip, updated to {updatedVersion}");
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(WaitForEvent);
|
||||
@@ -53,26 +81,48 @@ namespace Jackett.Tray
|
||||
|
||||
private void WaitForEvent()
|
||||
{
|
||||
Engine.LockService.WaitForSignal();
|
||||
Application.Exit();
|
||||
trayLockService.WaitForSignal();
|
||||
logger.Info("Received signal from tray lock service");
|
||||
|
||||
if (windowsService.ServiceExists() && windowsService.ServiceRunning())
|
||||
{
|
||||
//We won't be able to start the tray app up again from the updater, as when running via a windows service there is no interaction with the desktop
|
||||
//Fire off a console process that will start the tray 20 seconds later
|
||||
|
||||
string trayExePath = Assembly.GetEntryAssembly().Location;
|
||||
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
Arguments = $"/c timeout 20 > NUL & \"{trayExePath}\" --UpdatedVersion yes",
|
||||
FileName = "cmd.exe",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
|
||||
logger.Info("Starting 20 second delay tray launch as Jackett is running as a Windows service: " + startInfo.FileName + " " + startInfo.Arguments);
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
|
||||
CloseTrayApplication();
|
||||
}
|
||||
|
||||
void toolStripMenuItemWebUI_Click(object sender, EventArgs e)
|
||||
private void toolStripMenuItemWebUI_Click(object sender, EventArgs e)
|
||||
{
|
||||
Process.Start("http://127.0.0.1:" + Engine.ServerConfig.Port);
|
||||
Process.Start("http://127.0.0.1:" + serverConfig.Port);
|
||||
}
|
||||
|
||||
void toolStripMenuItemShutdown_Click(object sender, EventArgs e)
|
||||
private void toolStripMenuItemShutdown_Click(object sender, EventArgs e)
|
||||
{
|
||||
Process.GetCurrentProcess().Kill();
|
||||
CloseTrayApplication();
|
||||
}
|
||||
|
||||
void toolStripMenuItemAutoStart_CheckedChanged(object sender, EventArgs e)
|
||||
private void toolStripMenuItemAutoStart_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
AutoStart = toolStripMenuItemAutoStart.Checked;
|
||||
}
|
||||
|
||||
string ProgramTitle
|
||||
private string ProgramTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -80,7 +130,7 @@ namespace Jackett.Tray
|
||||
}
|
||||
}
|
||||
|
||||
bool AutoStart
|
||||
private bool AutoStart
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -122,22 +172,29 @@ namespace Jackett.Tray
|
||||
|
||||
private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
|
||||
{
|
||||
if (Engine.ServiceConfig.ServiceExists())
|
||||
if (windowsService.ServiceExists())
|
||||
{
|
||||
backgroundMenuItem.Visible = true;
|
||||
serviceControlMenuItem.Visible = true;
|
||||
toolStripSeparator1.Visible = true;
|
||||
toolStripSeparator2.Visible = true;
|
||||
if (Engine.ServiceConfig.ServiceRunning())
|
||||
|
||||
if (windowsService.ServiceRunning())
|
||||
{
|
||||
serviceControlMenuItem.Text = "Stop background service";
|
||||
} else
|
||||
backgroundMenuItem.Text = "Jackett is running as a background service";
|
||||
toolStripMenuItemWebUI.Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
serviceControlMenuItem.Text = "Start background service";
|
||||
backgroundMenuItem.Text = "Jackett will run as a background service";
|
||||
toolStripMenuItemWebUI.Enabled = false;
|
||||
}
|
||||
|
||||
toolStripMenuItemShutdown.Text = "Close tray icon";
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundMenuItem.Visible = false;
|
||||
serviceControlMenuItem.Visible = false;
|
||||
@@ -151,17 +208,17 @@ namespace Jackett.Tray
|
||||
{
|
||||
var consolePath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "JackettConsole.exe");
|
||||
|
||||
if (Engine.ServiceConfig.ServiceRunning())
|
||||
if (windowsService.ServiceRunning())
|
||||
{
|
||||
if (ServerUtil.IsUserAdministrator())
|
||||
{
|
||||
Engine.ServiceConfig.Stop();
|
||||
|
||||
} else
|
||||
windowsService.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
Engine.ProcessService.StartProcessAndLog(consolePath, "--Stop", true);
|
||||
processService.StartProcessAndLog(consolePath, "--Stop", true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -173,13 +230,13 @@ namespace Jackett.Tray
|
||||
{
|
||||
if (ServerUtil.IsUserAdministrator())
|
||||
{
|
||||
Engine.ServiceConfig.Start();
|
||||
windowsService.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
Engine.ProcessService.StartProcessAndLog(consolePath, "--Start", true);
|
||||
processService.StartProcessAndLog(consolePath, "--Start", true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -188,5 +245,64 @@ namespace Jackett.Tray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseTrayApplication()
|
||||
{
|
||||
closeApplicationInitiated = true;
|
||||
|
||||
logger.Info("Close of tray application initiated");
|
||||
|
||||
//Clears notify icon, otherwise icon will still appear on taskbar until you hover the mouse over
|
||||
notifyIcon1.Icon = null;
|
||||
notifyIcon1.Dispose();
|
||||
Application.DoEvents();
|
||||
|
||||
if (consoleProcess != null && !consoleProcess.HasExited)
|
||||
{
|
||||
consoleProcess.StandardInput.Close();
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
if (consoleProcess != null && !consoleProcess.HasExited)
|
||||
{
|
||||
consoleProcess.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
Application.Exit();
|
||||
}
|
||||
|
||||
private void StartConsoleApplication()
|
||||
{
|
||||
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
|
||||
|
||||
var exePath = Path.Combine(applicationFolder, "JackettConsole.exe");
|
||||
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = exePath,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
consoleProcess = Process.Start(startInfo);
|
||||
consoleProcess.EnableRaisingEvents = true;
|
||||
consoleProcess.Exited += ProcessExited;
|
||||
consoleProcess.ErrorDataReceived += ProcessErrorDataReceived;
|
||||
}
|
||||
|
||||
private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
logger.Error(e.Data);
|
||||
}
|
||||
|
||||
private void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
if (!closeApplicationInitiated)
|
||||
{
|
||||
logger.Info("Tray icon not responsible for process exit");
|
||||
CloseTrayApplication();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
@@ -11,7 +12,7 @@ namespace Jackett.Tray
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var JacketTrayProcess = Process.GetCurrentProcess();
|
||||
var runningProcesses = Process.GetProcesses();
|
||||
@@ -23,10 +24,29 @@ namespace Jackett.Tray
|
||||
MessageBox.Show("JackettTray is already running");
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
string newVersion = "";
|
||||
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
|
||||
|
||||
try
|
||||
{
|
||||
var optionsResult = commandLineParser.ParseArguments<TrayConsoleOptions>(args);
|
||||
optionsResult.WithParsed(options =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(options.UpdatedVersion))
|
||||
{
|
||||
newVersion = options.UpdatedVersion;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
newVersion = "";
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new Main());
|
||||
Application.Run(new Main(newVersion));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
src/Jackett.Tray/Properties/Settings.Designer.cs
generated
2
src/Jackett.Tray/Properties/Settings.Designer.cs
generated
@@ -12,7 +12,7 @@ namespace Jackett.Tray.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.3.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
10
src/Jackett.Tray/TrayConsoleOptions.cs
Normal file
10
src/Jackett.Tray/TrayConsoleOptions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace Jackett.Tray
|
||||
{
|
||||
public class TrayConsoleOptions
|
||||
{
|
||||
[Option("UpdatedVersion", HelpText = "Indicates the new version that Jackett just updated to so that user understands why they are getting a prompt to start Windows service")]
|
||||
public string UpdatedVersion { get; set; }
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
|
||||
</startup>
|
||||
|
||||
</configuration>
|
@@ -1,92 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Jackett.Updater</RootNamespace>
|
||||
<AssemblyName>JackettUpdater</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
<RuntimeIdentifier>win</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net452;net461;netcoreapp2.1</TargetFrameworks>
|
||||
<ApplicationIcon>jackett.ico</ApplicationIcon>
|
||||
<AssemblyName>JackettUpdater</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Version>0.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Net.Http.WebRequest" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.ServiceModel" />
|
||||
<Reference Include="System.Transactions" />
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UpdaterConsoleOptions.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj">
|
||||
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
|
||||
<Name>Jackett.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett.Service\Jackett.Service.csproj">
|
||||
<Project>{bf611f7b-4658-4cb8-aa9e-0736fadaa3ba}</Project>
|
||||
<Name>Jackett.Service</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Jackett\Jackett.csproj">
|
||||
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
|
||||
<Name>Jackett</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="jackett.ico" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
|
||||
</Project>
|
@@ -1,34 +1,59 @@
|
||||
using CommandLine;
|
||||
using CommandLine.Text;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Services;
|
||||
using Jackett.Common.Services;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Web;
|
||||
using Jackett.Common;
|
||||
|
||||
namespace Jackett.Updater
|
||||
{
|
||||
class Program
|
||||
public class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
private IProcessService processService;
|
||||
private IServiceConfigService windowsService;
|
||||
private Logger logger;
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
new Program().Run(args);
|
||||
}
|
||||
|
||||
private void Run(string[] args)
|
||||
{
|
||||
Engine.BuildContainer(new RuntimeSettings()
|
||||
RuntimeSettings runtimeSettings = new RuntimeSettings()
|
||||
{
|
||||
CustomLogFileName = "updater.txt"
|
||||
});
|
||||
Engine.Logger.Info("Jackett Updater v" + GetCurrentVersion());
|
||||
Engine.Logger.Info("Options \"" + string.Join("\" \"", args) + "\"");
|
||||
try {
|
||||
var optionsResult = Parser.Default.ParseArguments<UpdaterConsoleOptions>(args);
|
||||
};
|
||||
|
||||
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
|
||||
logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
logger.Info("Jackett Updater v" + GetCurrentVersion());
|
||||
logger.Info("Options \"" + string.Join("\" \"", args) + "\"");
|
||||
|
||||
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
if (isWindows)
|
||||
{
|
||||
//The updater starts before Jackett closes
|
||||
logger.Info("Pausing for 3 seconds to give Jackett & tray time to shutdown");
|
||||
System.Threading.Thread.Sleep(3000);
|
||||
}
|
||||
|
||||
processService = new ProcessService(logger);
|
||||
windowsService = new WindowsServiceConfigService(processService, logger);
|
||||
|
||||
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
|
||||
|
||||
try
|
||||
{
|
||||
var optionsResult = commandLineParser.ParseArguments<UpdaterConsoleOptions>(args);
|
||||
optionsResult.WithParsed(options =>
|
||||
{
|
||||
ProcessUpdate(options);
|
||||
@@ -36,14 +61,14 @@ namespace Jackett.Updater
|
||||
);
|
||||
optionsResult.WithNotParsed(errors =>
|
||||
{
|
||||
Engine.Logger.Error(HelpText.AutoBuild(optionsResult));
|
||||
Engine.Logger.Error("Failed to process update arguments!");
|
||||
logger.Error(HelpText.AutoBuild(optionsResult));
|
||||
logger.Error("Failed to process update arguments!");
|
||||
Console.ReadKey();
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Engine.Logger.Error(e, "Exception applying update!");
|
||||
logger.Error(e, "Exception applying update!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,21 +86,20 @@ namespace Jackett.Updater
|
||||
try
|
||||
{
|
||||
var proc = Process.GetProcessById(pid);
|
||||
Engine.Logger.Info("Killing process " + proc.Id);
|
||||
logger.Info("Killing process " + proc.Id);
|
||||
proc.Kill();
|
||||
var exited = proc.WaitForExit(5000);
|
||||
if (!exited)
|
||||
Engine.Logger.Info("Process " + pid.ToString() + " didn't exit within 5 seconds");
|
||||
|
||||
logger.Info("Process " + pid.ToString() + " didn't exit within 5 seconds");
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Engine.Logger.Info("Process " + pid.ToString() + " is already dead");
|
||||
logger.Info("Process " + pid.ToString() + " is already dead");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Engine.Logger.Info("Error killing process " + pid.ToString());
|
||||
Engine.Logger.Info(e);
|
||||
logger.Info("Error killing process " + pid.ToString());
|
||||
logger.Info(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +107,7 @@ namespace Jackett.Updater
|
||||
private void ProcessUpdate(UpdaterConsoleOptions options)
|
||||
{
|
||||
var updateLocation = GetUpdateLocation();
|
||||
if(!(updateLocation.EndsWith("\\") || updateLocation.EndsWith("/")))
|
||||
if (!(updateLocation.EndsWith("\\") || updateLocation.EndsWith("/")))
|
||||
{
|
||||
updateLocation += Path.DirectorySeparatorChar;
|
||||
}
|
||||
@@ -95,18 +119,18 @@ namespace Jackett.Updater
|
||||
pids = Array.ConvertAll(pidsStr, pid => int.Parse(pid));
|
||||
}
|
||||
|
||||
var isWindows = System.Environment.OSVersion.Platform != PlatformID.Unix;
|
||||
var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
var trayRunning = false;
|
||||
var trayProcesses = Process.GetProcessesByName("JackettTray");
|
||||
if (isWindows)
|
||||
{
|
||||
if (trayProcesses.Count() > 0)
|
||||
{
|
||||
{
|
||||
foreach (var proc in trayProcesses)
|
||||
{
|
||||
try
|
||||
{
|
||||
Engine.Logger.Info("Killing tray process " + proc.Id);
|
||||
logger.Info("Killing tray process " + proc.Id);
|
||||
proc.Kill();
|
||||
trayRunning = true;
|
||||
}
|
||||
@@ -118,9 +142,9 @@ namespace Jackett.Updater
|
||||
// On unix we kill the PIDs after the update so e.g. systemd can automatically restart the process
|
||||
KillPids(pids);
|
||||
}
|
||||
Engine.Logger.Info("Finding files in: " + updateLocation);
|
||||
logger.Info("Finding files in: " + updateLocation);
|
||||
var files = Directory.GetFiles(updateLocation, "*.*", SearchOption.AllDirectories);
|
||||
foreach(var file in files)
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileName = Path.GetFileName(file).ToLowerInvariant();
|
||||
|
||||
@@ -130,20 +154,21 @@ namespace Jackett.Updater
|
||||
{
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Engine.Logger.Info("Copying " + fileName);
|
||||
try
|
||||
{
|
||||
logger.Info("Copying " + fileName);
|
||||
var dest = Path.Combine(options.Path, file.Substring(updateLocation.Length));
|
||||
var destDir = Path.GetDirectoryName(dest);
|
||||
if (!Directory.Exists(destDir))
|
||||
{
|
||||
Engine.Logger.Info("Creating directory " + destDir);
|
||||
logger.Info("Creating directory " + destDir);
|
||||
Directory.CreateDirectory(destDir);
|
||||
}
|
||||
File.Copy(file, dest, true);
|
||||
}
|
||||
catch(Exception e)
|
||||
catch (Exception e)
|
||||
{
|
||||
Engine.Logger.Error(e);
|
||||
logger.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,17 +182,16 @@ namespace Jackett.Updater
|
||||
var deleteDir = Path.Combine(options.Path, oldDir);
|
||||
if (Directory.Exists(deleteDir))
|
||||
{
|
||||
Engine.Logger.Info("Deleting directory " + deleteDir);
|
||||
logger.Info("Deleting directory " + deleteDir);
|
||||
Directory.Delete(deleteDir, true);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Engine.Logger.Error(e);
|
||||
logger.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// delete old files
|
||||
string[] oldFiles = new string[] {
|
||||
"Content/css/jquery.dataTables.css",
|
||||
@@ -209,20 +233,20 @@ namespace Jackett.Updater
|
||||
"Definitions/torrentwtf.yml",
|
||||
};
|
||||
|
||||
foreach (var oldFIle in oldFiles)
|
||||
foreach (var oldFile in oldFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleteFile = Path.Combine(options.Path, oldFIle);
|
||||
var deleteFile = Path.Combine(options.Path, oldFile);
|
||||
if (File.Exists(deleteFile))
|
||||
{
|
||||
Engine.Logger.Info("Deleting file " + deleteFile);
|
||||
logger.Info("Deleting file " + deleteFile);
|
||||
File.Delete(deleteFile);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Engine.Logger.Error(e);
|
||||
logger.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,26 +256,48 @@ namespace Jackett.Updater
|
||||
|
||||
if (options.NoRestart == false)
|
||||
{
|
||||
if (trayRunning)
|
||||
if (isWindows && (trayRunning || options.StartTray) && !string.Equals(options.Type, "WindowsService", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
Arguments = options.Args,
|
||||
Arguments = $"--UpdatedVersion \" {EnvironmentUtil.JackettVersion}\"",
|
||||
FileName = Path.Combine(options.Path, "JackettTray.exe"),
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
logger.Info("Starting Tray: " + startInfo.FileName + " " + startInfo.Arguments);
|
||||
Process.Start(startInfo);
|
||||
|
||||
if (!windowsService.ServiceExists())
|
||||
{
|
||||
//User was running the tray icon, but not using the Windows service, starting Tray icon will start JackettConsole as well
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(string.Equals(options.Type, "JackettService.exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
if (string.Equals(options.Type, "WindowsService", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var serviceHelper = new ServiceConfigService(null, null);
|
||||
if (serviceHelper.ServiceExists())
|
||||
logger.Info("Starting Windows service");
|
||||
|
||||
if (ServerUtil.IsUserAdministrator())
|
||||
{
|
||||
serviceHelper.Start();
|
||||
windowsService.Start();
|
||||
}
|
||||
} else
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var consolePath = Path.Combine(options.Path, "JackettConsole.exe");
|
||||
processService.StartProcessAndLog(consolePath, "--Start", true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Error("Failed to get admin rights to start the service.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
var startInfo = new ProcessStartInfo()
|
||||
{
|
||||
@@ -260,13 +306,21 @@ namespace Jackett.Updater
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
if (!isWindows)
|
||||
if (isWindows)
|
||||
{
|
||||
//User didn't initiate the update from Windows service and wasn't running Jackett via the tray, must have started from the console
|
||||
startInfo.Arguments = $"/K {startInfo.FileName} {startInfo.Arguments}";
|
||||
startInfo.FileName = "cmd.exe";
|
||||
startInfo.CreateNoWindow = false;
|
||||
startInfo.WindowStyle = ProcessWindowStyle.Normal;
|
||||
}
|
||||
else
|
||||
{
|
||||
startInfo.Arguments = startInfo.FileName + " " + startInfo.Arguments;
|
||||
startInfo.FileName = "mono";
|
||||
}
|
||||
|
||||
Engine.Logger.Info("Starting Jackett: " + startInfo.FileName + " " + startInfo.Arguments);
|
||||
logger.Info("Starting Jackett: " + startInfo.FileName + " " + startInfo.Arguments);
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
}
|
||||
@@ -275,7 +329,7 @@ namespace Jackett.Updater
|
||||
private string GetUpdateLocation()
|
||||
{
|
||||
var location = new Uri(Assembly.GetEntryAssembly().GetName().CodeBase);
|
||||
return new FileInfo(HttpUtility.UrlDecode(location.AbsolutePath)).DirectoryName;
|
||||
return new FileInfo(WebUtility.UrlDecode(location.AbsolutePath)).DirectoryName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,35 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Jackett.Updater")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Jackett.Updater")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2015")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("a61e311a-6f8b-4497-b5e4-2ea8994c7bd8")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("0.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("0.0.0.0")]
|
@@ -18,5 +18,8 @@ namespace Jackett.Updater
|
||||
|
||||
[Option("KillPids", HelpText = "PIDs which will be killed before (Windows) or after (Unix) the update")]
|
||||
public string KillPids { get; set; }
|
||||
|
||||
[Option("StartTray", HelpText = "Indicates that the updater should start the tray icon")]
|
||||
public bool StartTray { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Service", "Jackett.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Tray", "Jackett.Tray\Jackett.Tray.csproj", "{FF9025B1-EC14-4AA9-8081-9F69C5E35B63}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Updater", "Jackett.Updater\Jackett.Updater.csproj", "{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Updater", "Jackett.Updater\Jackett.Updater.csproj", "{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Test", "Jackett.Test\Jackett.Test.csproj", "{FA22C904-9F5D-4D3C-9122-3E33652E7373}"
|
||||
EndProject
|
||||
@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET Core", ".NET Core", "{
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{AA50F785-12B8-4669-8D4F-EAFB49258E60}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Server", "Jackett.Server\Jackett.Server.csproj", "{84182782-EDBC-4342-ADA6-72B7694D0862}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -81,6 +83,10 @@ Global
|
||||
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -98,6 +104,7 @@ Global
|
||||
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73} = {2FA9B879-5882-4B39-8D34-9EBCB82B4F2B}
|
||||
{FF8B9A1B-AE7E-4F14-9C37-DA861D034738} = {AA50F785-12B8-4669-8D4F-EAFB49258E60}
|
||||
{6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9} = {AA50F785-12B8-4669-8D4F-EAFB49258E60}
|
||||
{84182782-EDBC-4342-ADA6-72B7694D0862} = {6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {54BC4102-8B85-49C1-BA12-257D941D1B97}
|
||||
|
@@ -440,7 +440,7 @@ namespace Jackett.Controllers
|
||||
torrent_id = release.Guid.ToString(),
|
||||
details_url = release.Comments.ToString(),
|
||||
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
|
||||
imdb_id = release.Imdb.HasValue ? "tt" + release.Imdb : null,
|
||||
imdb_id = release.Imdb.HasValue ? ParseUtil.GetFullImdbID("tt" + release.Imdb) : null,
|
||||
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
|
||||
type = "movie",
|
||||
size = (long)release.Size / (1024 * 1024), // This is in MB
|
||||
|
@@ -91,6 +91,7 @@ namespace Jackett.Controllers
|
||||
}
|
||||
|
||||
string omdbApiKey = config.omdbkey;
|
||||
string omdbApiUrl = config.omdburl;
|
||||
|
||||
serverConfig.UpdateDisabled = updateDisabled;
|
||||
serverConfig.UpdatePrerelease = preRelease;
|
||||
@@ -101,9 +102,10 @@ namespace Jackett.Controllers
|
||||
Engine.SetLogLevel(logging ? LogLevel.Debug : LogLevel.Info);
|
||||
serverConfig.RuntimeSettings.TracingEnabled = logging;
|
||||
|
||||
if (omdbApiKey != serverConfig.OmdbApiKey)
|
||||
if (omdbApiKey != serverConfig.OmdbApiKey || omdbApiUrl != serverConfig.OmdbApiUrl)
|
||||
{
|
||||
serverConfig.OmdbApiKey = omdbApiKey;
|
||||
serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/');
|
||||
configService.SaveConfig(serverConfig);
|
||||
// HACK
|
||||
indexerService.InitAggregateIndexer();
|
||||
|
Reference in New Issue
Block a user