mirror of
https://github.com/Jackett/Jackett.git
synced 2025-12-24 14:57:09 +01:00
Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f00d8e192a | ||
|
|
9ca4600eab | ||
|
|
5e8ebd8579 | ||
|
|
748881ef70 | ||
|
|
42e6600c6a | ||
|
|
85d17a7763 | ||
|
|
b1391b0523 | ||
|
|
8510a42d7a | ||
|
|
e61f6e78b3 | ||
|
|
176ba5a6db | ||
|
|
ad3c56e83b | ||
|
|
2495a2f64e | ||
|
|
0d6830b0aa | ||
|
|
ef316590cb | ||
|
|
c3c25190a2 | ||
|
|
fc3d8d1aec | ||
|
|
bbb7d1c3c7 | ||
|
|
89dfac3009 | ||
|
|
9610965979 | ||
|
|
8b501403e1 | ||
|
|
3a88aeb649 | ||
|
|
0a0ba2291b | ||
|
|
2484e22da9 | ||
|
|
e8ca3e6b52 | ||
|
|
d5c7445919 | ||
|
|
72045404d4 | ||
|
|
1a6e1a8c60 | ||
|
|
540dc0fad4 | ||
|
|
1db3e93ce1 | ||
|
|
a5ec65bff3 | ||
|
|
c4e3aa8a64 | ||
|
|
e3e183d86b | ||
|
|
bfe892f2c8 | ||
|
|
b28116c10f | ||
|
|
52c408fce7 | ||
|
|
a70997ad7f | ||
|
|
8840de316d | ||
|
|
409483e680 | ||
|
|
b19d690305 | ||
|
|
0fabaf2fe0 | ||
|
|
3035d8b901 | ||
|
|
f3d73b5661 | ||
|
|
f335dbf7bc | ||
|
|
b5178dc7b8 | ||
|
|
ad09a4dd77 | ||
|
|
da0ead13f4 | ||
|
|
4b6a9376de | ||
|
|
3eab605423 | ||
|
|
7fc37f1156 | ||
|
|
9f03f8129a | ||
|
|
51aa6fdf13 | ||
|
|
f2f602dcc5 | ||
|
|
a217381668 | ||
|
|
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 | ||
|
|
cf3848a54f | ||
|
|
1a14e8dd4b | ||
|
|
a25eb7f951 | ||
|
|
9437cd54d3 | ||
|
|
be55c5e4a6 | ||
|
|
f1d774aa07 | ||
|
|
c109133fcc | ||
|
|
3538fdfaf7 | ||
|
|
3468e7d404 | ||
|
|
ec4afda184 | ||
|
|
67b1835264 | ||
|
|
aee64aa589 | ||
|
|
687e6e237f | ||
|
|
b48dd5e930 | ||
|
|
5ee6833610 | ||
|
|
c998ba3762 | ||
|
|
2d4f7ab0e9 | ||
|
|
676d03eb88 | ||
|
|
68351a480b | ||
|
|
33e35f1bd3 | ||
|
|
e82b54994e | ||
|
|
6f7ecbfb7b | ||
|
|
c4aa49eb32 | ||
|
|
32aae44ffc | ||
|
|
7883534c5e | ||
|
|
b58c9fb718 | ||
|
|
99d8f63f9e | ||
|
|
635e8240d2 | ||
|
|
117a670aa3 | ||
|
|
f49c58a1fa | ||
|
|
2492f1b797 | ||
|
|
d6781f67b2 | ||
|
|
2e0c22eb6d | ||
|
|
211e152863 | ||
|
|
f7bf4060ea | ||
|
|
8c953bbf01 | ||
|
|
4e91761fdf | ||
|
|
53f8465e67 | ||
|
|
5818b914a3 | ||
|
|
0680d39d90 | ||
|
|
4ca6676be0 | ||
|
|
3d85e751b7 | ||
|
|
965da06214 | ||
|
|
e8bc2816ef | ||
|
|
28ed7cc8a5 | ||
|
|
089d9f2e3d | ||
|
|
9abce7a586 | ||
|
|
53162b4bd3 | ||
|
|
80d78a027b | ||
|
|
b4eda2ed54 | ||
|
|
4d8d21a815 | ||
|
|
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -200,3 +200,4 @@ FakesAssemblies/
|
||||
/TestResults
|
||||
*.DS_Store
|
||||
.idea/
|
||||
launchSettings.json
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
49
README.md
49
README.md
@@ -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,7 +42,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* KickAssTorrent (thekat.se clone)
|
||||
* LimeTorrents
|
||||
* MagnetDL
|
||||
* NextTorrent
|
||||
* MejorTorrent <!-- maintained by ivandelabeldad -->
|
||||
* Newpct (aka: tvsinpagar, descargas2020, torrentlocura, torrentrapid, etc)
|
||||
* Nyaa.si
|
||||
* Nyaa-Pantsu
|
||||
@@ -70,6 +70,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* CzTorrent
|
||||
* Deildu
|
||||
* Gay-Torrents.net
|
||||
* Gay-Torrents.org
|
||||
* Kinozal
|
||||
* LostFilm.tv
|
||||
* Mega-Bliz
|
||||
@@ -125,7 +126,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* BroadcastTheNet
|
||||
* BrokenStones
|
||||
* BTNext
|
||||
* BTWorld
|
||||
* BTXpress
|
||||
* Carpathians
|
||||
* CCFBits
|
||||
* CGPeers
|
||||
@@ -146,7 +147,6 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* Elit Tracker
|
||||
* Elite-Tracker
|
||||
* Empornium
|
||||
* EoT-Forum
|
||||
* eStone
|
||||
* Ethor.net (Thor's Land)
|
||||
* FANO.IN
|
||||
@@ -157,7 +157,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* FunFile
|
||||
* FunkyTorrents
|
||||
* Fuzer
|
||||
* GayTorrent.ru
|
||||
* GAYtorrent.ru
|
||||
* GazelleGames
|
||||
* Gfxnews
|
||||
* GFXPeers
|
||||
@@ -171,6 +171,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* HD-Forever
|
||||
* HD-Only
|
||||
* HD-Space
|
||||
* HD-Spain
|
||||
* HD-Torrents
|
||||
* HD-Bits.com
|
||||
* HDBits
|
||||
@@ -240,9 +241,9 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* SportsCult
|
||||
* SportHD
|
||||
* Superbits
|
||||
* Synthesiz3r
|
||||
* Tasmanit
|
||||
* TBPlus
|
||||
* TehConnection.me
|
||||
* TenYardTracker
|
||||
* The Empire
|
||||
* The Geeks
|
||||
@@ -271,7 +272,6 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* TorrentSeeds
|
||||
* Torrent-Syndikat
|
||||
* TOrrent-tuRK (TORK)
|
||||
* TorrentWTF
|
||||
* TorViet
|
||||
* ToTheGlory
|
||||
* TranceTraffic
|
||||
@@ -284,11 +284,12 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
|
||||
* Waffles
|
||||
* World-In-HD
|
||||
* WorldOfP2P
|
||||
* x-ite.me
|
||||
* x264
|
||||
* xBytesV2
|
||||
* XSpeeds
|
||||
* Xthor
|
||||
* Your Exotic Torrents
|
||||
* ExoticaZ (Your Exotic Torrents)
|
||||
* Zamunda.net
|
||||
* Zelka.org
|
||||
|
||||
@@ -337,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
|
||||
@@ -370,6 +371,19 @@ Example config for apache:
|
||||
</Location>
|
||||
```
|
||||
|
||||
Example config for nginx:
|
||||
```
|
||||
location /jackett {
|
||||
proxy_pass http://127.0.0.1:9117;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_redirect off;
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* __Command line switches__
|
||||
@@ -403,11 +417,28 @@ 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
|
||||
* Build/Start the project
|
||||
|
||||
### OSX
|
||||
NOTE: msbuild is included in the mono release.
|
||||
* Install Homebrew https://brew.sh
|
||||
* open terminal
|
||||
* brew install nuget mono
|
||||
* git clone https://github.com/Jackett/Jackett.git
|
||||
* cd Jackett/src
|
||||
* nuget restore Jackett.sln
|
||||
|
||||
NOTE: if you get the error "NU1102: Unable to find package Microsoft.AspNetCore with version (>= 2.1.2)" while restoring packages, you'll need to install it manually.https://www.microsoft.com/net/download/thank-you/dotnet-sdk-2.1.302-macos-x64-installer then delete the entire project directory and restart from a new clone
|
||||
|
||||
* msbuild Jackett.Console/Jackett.Console.csproj /t:Build /p:Configuration=Debug
|
||||
* curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync --user /dev/stdin
|
||||
* mono Jackett.Console/bin/Debug/JackettConsole.exe
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: 0.8.{build}
|
||||
version: 0.10.{build}
|
||||
skip_tags: true
|
||||
image: Visual Studio 2017
|
||||
configuration: Release
|
||||
@@ -27,6 +27,7 @@ deploy:
|
||||
description: $(release_description)
|
||||
auth_token:
|
||||
secure: hOg+16YTIbq4kO9u4D1YVOTbWDqgCX6mAQYMbnmBBSw2CiUsZh7OKbupoUb3FtWa
|
||||
artifact: /^(?:(?![Ee]xperimental).)*$/
|
||||
draft: true
|
||||
on:
|
||||
branch: master
|
||||
|
||||
156
build.cake
156
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);
|
||||
@@ -45,22 +45,21 @@ Task("Clean")
|
||||
Information("Clean completed");
|
||||
});
|
||||
|
||||
Task("Restore-NuGet-Packages")
|
||||
Task("Build-Kestrel-Full-Framework")
|
||||
.IsDependentOn("Clean")
|
||||
.Does(() =>
|
||||
{
|
||||
NuGetRestore("./src/Jackett.sln");
|
||||
});
|
||||
|
||||
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")
|
||||
.IsDependentOn("Build")
|
||||
.IsDependentOn("Build-Kestrel-Full-Framework")
|
||||
.Does(() =>
|
||||
{
|
||||
CreateDirectory("./" + testResultsDirName);
|
||||
@@ -77,31 +76,8 @@ Task("Run-Unit-Tests")
|
||||
}
|
||||
});
|
||||
|
||||
Task("Copy-Files-Full-Framework")
|
||||
.IsDependentOn("Run-Unit-Tests")
|
||||
.Does(() =>
|
||||
{
|
||||
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("./Upstart.config", windowsOutput);
|
||||
CopyFiles("./LICENSE", windowsOutput);
|
||||
CopyFiles("./README.md", windowsOutput);
|
||||
|
||||
var monoOutput = monoBuildFullFramework + "/Jackett";
|
||||
|
||||
CopyDirectory(windowsBuildFullFramework, monoBuildFullFramework);
|
||||
DeleteFiles(monoOutput + "/JackettService.*");
|
||||
DeleteFiles(monoOutput + "/JackettTray.*");
|
||||
|
||||
Information("Full framework file copy completed");
|
||||
});
|
||||
|
||||
Task("Check-Packaging-Platform")
|
||||
.IsDependentOn("Copy-Files-Full-Framework")
|
||||
.IsDependentOn("Run-Unit-Tests")
|
||||
.Does(() =>
|
||||
{
|
||||
if (IsRunningOnWindows())
|
||||
@@ -115,49 +91,86 @@ Task("Check-Packaging-Platform")
|
||||
}
|
||||
});
|
||||
|
||||
Task("Package-Windows-Installer-Full-Framework")
|
||||
Task("Experimental-Kestrel-Windows-Full-Framework")
|
||||
.IsDependentOn("Check-Packaging-Platform")
|
||||
.Does(() =>
|
||||
{
|
||||
InnoSetup("./Installer.iss", new InnoSetupSettings {
|
||||
OutputDirectory = workingDir + "/" + artifactsDirName
|
||||
});
|
||||
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("Package-Files-Full-Framework-Windows")
|
||||
Task("Experimental-Kestrel-Mono-Full-Framework")
|
||||
.IsDependentOn("Check-Packaging-Platform")
|
||||
.Does(() =>
|
||||
{
|
||||
Zip(windowsBuildFullFramework, $"./{artifactsDirName}/Jackett.Binaries.Windows.zip");
|
||||
Information(@"Full Framework Windows Binaries Zipping Completed");
|
||||
});
|
||||
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
|
||||
string buildOutputPath = "./BuildOutput/Experimental/net461/linux-x64/Jackett";
|
||||
|
||||
Task("Package-Files-Full-Framework-Mono")
|
||||
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", "Jackett.Binaries.Mono.tar.gz");
|
||||
});
|
||||
|
||||
Task("Experimental-DotNetCore")
|
||||
.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;
|
||||
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");
|
||||
|
||||
RunCygwinCommand("Tar", tarArguments);
|
||||
RunCygwinCommand("Gzip", gzipArguments);
|
||||
|
||||
MoveFile($"{monoBuildFullFramework}/{tarFileName}.gz", $"./{artifactsDirName}/{tarFileName}.gz");
|
||||
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("Package-Full-Framework")
|
||||
.IsDependentOn("Package-Windows-Installer-Full-Framework")
|
||||
.IsDependentOn("Package-Files-Full-Framework-Windows")
|
||||
.IsDependentOn("Package-Files-Full-Framework-Mono")
|
||||
Task("Experimental")
|
||||
.IsDependentOn("Experimental-Kestrel-Windows-Full-Framework")
|
||||
.IsDependentOn("Experimental-Kestrel-Mono-Full-Framework")
|
||||
//.IsDependentOn("Experimental-DotNetCore")
|
||||
.Does(() =>
|
||||
{
|
||||
Information("Full Framwork Packaging Completed");
|
||||
Information("Experimental builds completed");
|
||||
});
|
||||
|
||||
Task("Appveyor-Push-Artifacts")
|
||||
.IsDependentOn("Package-Full-Framework")
|
||||
.IsDependentOn("Experimental")
|
||||
.Does(() =>
|
||||
{
|
||||
if (AppVeyor.IsRunningOnAppVeyor)
|
||||
@@ -263,10 +276,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 != '') {
|
||||
@@ -737,9 +738,9 @@ function updateReleasesRow(row)
|
||||
if (DownloadVolumeFactor == 0) {
|
||||
labels.append('\n<span class="label label-success">FREELEECH</span>');
|
||||
} else if (DownloadVolumeFactor < 1) {
|
||||
labels.append('\n<span class="label label-primary">' + DownloadVolumeFactor * 100 + '%DL</span>');
|
||||
labels.append('\n<span class="label label-primary">' + (DownloadVolumeFactor * 100).toFixed(0) + '%DL</span>');
|
||||
} else if (DownloadVolumeFactor > 1) {
|
||||
labels.append('\n<span class="label label-danger">' + DownloadVolumeFactor * 100 + '%DL</span>');
|
||||
labels.append('\n<span class="label label-danger">' + (DownloadVolumeFactor * 100).toFixed(0) + '%DL</span>');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -747,7 +748,7 @@ function updateReleasesRow(row)
|
||||
if (UploadVolumeFactor == 0) {
|
||||
labels.append('\n<span class="label label-warning">NO UPLOAD</span>');
|
||||
} else if (UploadVolumeFactor != 1) {
|
||||
labels.append('\n<span class="label label-info">' + UploadVolumeFactor * 100 + '%UL</span>');
|
||||
labels.append('\n<span class="label label-info">' + (UploadVolumeFactor * 100).toFixed(0) + '%UL</span>');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<span class="input-header">Proxy password: </span>
|
||||
<input id="jackett-proxy-password" class="form-control input-right" type="text" value="" placeholder="">
|
||||
<input id="jackett-proxy-password" class="form-control input-right" type="password" value="" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
@@ -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=20180710"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
args: id
|
||||
title:
|
||||
selector: td:nth-child(3) > a.torrent > span.span-1440
|
||||
filters:
|
||||
- name: re_replace # remove anidb id from return string
|
||||
args: ["(\\[[A-Z0-9]*\\])\\.", "."]
|
||||
details:
|
||||
selector: td:nth-child(3) > a.torrent
|
||||
attribute: href
|
||||
|
||||
@@ -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
|
||||
@@ -76,32 +92,34 @@
|
||||
selector: a[href^="details.php"]
|
||||
attribute: href
|
||||
download:
|
||||
selector: a[href^=" /gettorrent/ssl/"]
|
||||
selector: a[href^=" /gettorrent/"]
|
||||
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"
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
rows:
|
||||
selector: tr.browse_color, tr.freeleech_color, tr[id^="kdescr"]
|
||||
after: 1
|
||||
fields:
|
||||
fields: # some users (rank specific?) have an extra column (td:nth-child(4)) with bookmark features
|
||||
banner:
|
||||
selector: a[href^="details.php?id="][onmouseover]
|
||||
attribute: onmouseover
|
||||
@@ -98,20 +98,20 @@
|
||||
selector: a[href^="download.php"]
|
||||
attribute: href
|
||||
files:
|
||||
selector: td:nth-child(4)
|
||||
selector: a[href^="filelist.php"]
|
||||
size:
|
||||
selector: td:nth-child(7)
|
||||
selector: td:nth-last-child(6)
|
||||
grabs:
|
||||
selector: td:nth-child(8)
|
||||
selector: td:nth-last-child(5)
|
||||
filters:
|
||||
- name: regexp
|
||||
args: ([\d,]+)
|
||||
seeders:
|
||||
selector: td:nth-child(9)
|
||||
selector: td:nth-last-child(4)
|
||||
leechers:
|
||||
selector: td:nth-child(10)
|
||||
selector: td:nth-last-child(3)
|
||||
date:
|
||||
selector: td:nth-child(6)
|
||||
selector: td:nth-last-child(7)
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"a.info > b:contains(\"[FREE]\")": "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"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: "list/{{if .Keywords}}{{.Keywords}}{{else}}movie{{end}}.html"
|
||||
- path: "list/{{if .Keywords}}{{.Keywords}}{{else}}movie{{end}}/1-1-0.html"
|
||||
rows:
|
||||
selector: .rs
|
||||
fields:
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
site: btworld
|
||||
name: BTWorld
|
||||
description: "HD Movie/TV Tracker"
|
||||
site: btxpress
|
||||
name: BTXpress
|
||||
description: "HD Tracker Movies/TV"
|
||||
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"}
|
||||
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q, season, ep, imdbid]
|
||||
movie-search: [q, imdbid]
|
||||
|
||||
|
||||
login:
|
||||
path: /login
|
||||
method: form
|
||||
@@ -28,7 +28,7 @@
|
||||
- selector: table.main:contains("Login Failed!")
|
||||
test:
|
||||
path: /torrents
|
||||
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: /filterTorrents
|
||||
@@ -43,15 +43,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/"]
|
||||
@@ -62,37 +55,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"
|
||||
@@ -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
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
---
|
||||
# Update by LA5T based on the orignial 'cinematik.yml'
|
||||
# 29.07.2018 22:53 UTC+2
|
||||
#
|
||||
site: cinematik
|
||||
name: Cinematik
|
||||
description: "Non-Hollywood movie tracker"
|
||||
description: "Tracker for non-hollywood movies."
|
||||
language: en-us
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://www.cinematik.net
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: incldead
|
||||
type: select
|
||||
label: Status
|
||||
default: 1
|
||||
options:
|
||||
0: Active
|
||||
1: "Active and Inactive"
|
||||
2: Inactive
|
||||
- name: srchdtls
|
||||
type: checkbox
|
||||
label: "Detailed search"
|
||||
- name: info_results
|
||||
type: info
|
||||
label: "Search results"
|
||||
default: "You can increase the number of search results in your profile.<br />Default is 15."
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 1, cat: Movies, desc: "Comedy"}
|
||||
@@ -42,13 +68,15 @@
|
||||
- selector: table:contains("Login failed!")
|
||||
test:
|
||||
path: my.php
|
||||
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: browse.php
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}filter_cat[{{.}}]=1&{{end}}"
|
||||
$raw: "{{range .Categories}}c{{.}}=1&{{end}}"
|
||||
search: "{{ .Query.Keywords }}"
|
||||
incldead: "{{ .Config.incldead }}"
|
||||
srchdtls: "{{ if .Config.srchdtls }}1{{ else }}0{{ end }}"
|
||||
rows:
|
||||
selector: table[border="1"] tr:not(:first-child)
|
||||
fields:
|
||||
@@ -82,11 +110,11 @@
|
||||
selector: td:nth-child(11) div.addedtor
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"img[title=\"Golden Torrent: No Download Stats are Recorded\"]": "0"
|
||||
"img[title=\"Silver Torrent: Download Stats are 25% Recorded\"]": "0.25"
|
||||
"img[title=\"Platinum Torrent: No Download Stats are Recorded, Upload Stats are Doubled!\"]": "0"
|
||||
"*": "1"
|
||||
"img[title=\"Golden Torrent: No Download Stats are Recorded\"]": 0
|
||||
"img[title=\"Silver Torrent: Download Stats are 25% Recorded\"]": 0.25
|
||||
"img[title=\"Platinum Torrent: No Download Stats are Recorded, Upload Stats are Doubled!\"]": 0
|
||||
"*": 1
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
"img[title=\"Platinum Torrent: No Download Stats are Recorded, Upload Stats are Doubled!\"]": "2"
|
||||
"*": "1"
|
||||
"img[title=\"Platinum Torrent: No Download Stats are Recorded, Upload Stats are Doubled!\"]": 2
|
||||
"*": 1
|
||||
@@ -6,8 +6,10 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://www.cpabien.cm/
|
||||
- http://www.cpabien.io/
|
||||
legacylinks:
|
||||
- http://www.cpasbiens.cc/
|
||||
- http://www.cpabien.cm/
|
||||
- http://cpabien.cm/
|
||||
- http://cpasbiens1.com/
|
||||
- http://cpabien.mx/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
site: eotforum
|
||||
name: EoT-Forum
|
||||
description: "A German gerneral tracker"
|
||||
language: de-de
|
||||
type: private
|
||||
encoding: windows-1252
|
||||
links:
|
||||
- https://eot-forum.net
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
# Filme
|
||||
- {id: 14, cat: Movies/SD, desc: "SD XviD"}
|
||||
- {id: 15, cat: Movies/SD, desc: "SD x264"}
|
||||
- {id: 16, cat: Movies/HD, desc: "HD"}
|
||||
- {id: 68, cat: Movies/HD, desc: "UHD"}
|
||||
- {id: 17, cat: Movies/3D, desc: "3D"}
|
||||
- {id: 18, cat: Movies/DVD, desc: "DVD-R"}
|
||||
- {id: 19, cat: Movies, desc: "Pack"}
|
||||
- {id: 20, cat: Movies, desc: "International"}
|
||||
- {id: 21, cat: XXX, desc: "XXX"}
|
||||
|
||||
# Serien/TV
|
||||
- {id: 23, cat: TV/SD, desc: "SD XviD"}
|
||||
- {id: 24, cat: TV/SD, desc: "SD x264"}
|
||||
- {id: 25, cat: TV/HD, desc: "HD"}
|
||||
- {id: 26, cat: TV/SD, desc: "DVD-R"}
|
||||
- {id: 27, cat: TV, desc: "Pack"}
|
||||
- {id: 28, cat: TV, desc: "International"}
|
||||
- {id: 29, cat: TV/Sport, desc: "Sport"}
|
||||
|
||||
# Dokus
|
||||
- {id: 31, cat: TV/Documentary, desc: "SD XviD"}
|
||||
- {id: 32, cat: TV/Documentary, desc: "SD x264"}
|
||||
- {id: 33, cat: TV/Documentary, desc: "HD"}
|
||||
- {id: 34, cat: TV/Documentary, desc: "3D"}
|
||||
- {id: 35, cat: TV/Documentary, desc: "Pack"}
|
||||
- {id: 67, cat: TV/Documentary, desc: "DVD-R"}
|
||||
- {id: 36, cat: TV/Documentary, desc: "International"}
|
||||
|
||||
# Audio
|
||||
- {id: 38, cat: Audio, desc: "Charts"}
|
||||
- {id: 39, cat: Audio/MP3, desc: "MP3"}
|
||||
- {id: 40, cat: Audio/Lossless, desc: "Flac"}
|
||||
- {id: 41, cat: Audio, desc: "Pack"}
|
||||
- {id: 42, cat: Audio/Video, desc: "MusikVideo"}
|
||||
- {id: 43, cat: Audio/Audiobook, desc: "Hörbücher"}
|
||||
|
||||
# Spiele
|
||||
- {id: 45, cat: PC/Games, desc: "Windows"}
|
||||
- {id: 46, cat: PC/Mac, desc: "MacOS"}
|
||||
- {id: 47, cat: Console/PS4, desc: "Sony PS"}
|
||||
- {id: 48, cat: Console/Xbox , desc: "Microsoft XBox"}
|
||||
- {id: 49, cat: Console/NDS, desc: "Nintendo"}
|
||||
- {id: 50, cat: PC/Games, desc: "Linux"}
|
||||
- {id: 51, cat: Console, desc: "Andere"}
|
||||
|
||||
# Software
|
||||
- {id: 53, cat: PC, desc: "Windows"}
|
||||
- {id: 54, cat: PC/Mac, desc: "MacOS"}
|
||||
- {id: 55, cat: PC, desc: "Linux"}
|
||||
- {id: 56, cat: PC/Phone-Android, desc: "Android"}
|
||||
- {id: 57, cat: PC/Phone-IOS, desc: "Apple IOS"}
|
||||
- {id: 58, cat: PC/Phone-Other, desc: "Andere"}
|
||||
|
||||
# Sonstiges
|
||||
- {id: 60, cat: Books, desc: "EBooks"}
|
||||
- {id: 61, cat: Other, desc: "Bilder"}
|
||||
- {id: 62, cat: TV/Anime, desc: "Anime"}
|
||||
- {id: 63, cat: Other, desc: "MISC"}
|
||||
- {id: 64, cat: XXX, desc: "XXX-Bilder/EBooks/Audio"}
|
||||
|
||||
# EOT-Specials
|
||||
- {id: 66, cat: Other, desc: "Special"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q, season, ep]
|
||||
|
||||
login:
|
||||
path: index.php?page=login
|
||||
method: post
|
||||
inputs:
|
||||
uid: "{{ .Config.username }}"
|
||||
pwd: "{{ .Config.password }}"
|
||||
rememberme: "forever"
|
||||
submit: "Login"
|
||||
error:
|
||||
- selector: td.lista[align="center"][colspan="2"] > span
|
||||
test:
|
||||
path: index.php
|
||||
selector: a[href^="logout.php"]
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: index.php
|
||||
inputs:
|
||||
page: "torrents"
|
||||
search: "{{ .Query.Keywords }}"
|
||||
options: "0"
|
||||
active: "0"
|
||||
gold: "0"
|
||||
rows:
|
||||
selector: table.lista > tbody > tr:has(a[href^="index.php?page=torrent-details&id="])
|
||||
dateheaders:
|
||||
selector: ":has(td.header > b)"
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["Torrents vom ", ""]
|
||||
- name: replace
|
||||
args: ["Januar", "January"]
|
||||
- name: replace
|
||||
args: ["Februar", "February"]
|
||||
- name: replace
|
||||
args: ["März", "March"]
|
||||
- name: replace
|
||||
args: ["Mai", "May"]
|
||||
- name: replace
|
||||
args: ["Juni", "June"]
|
||||
- name: replace
|
||||
args: ["Juli", "July"]
|
||||
- name: replace
|
||||
args: ["Oktober", "October"]
|
||||
- name: replace
|
||||
args: ["Dezember", "December"]
|
||||
- name: dateparse
|
||||
args: "02.January.2006"
|
||||
fields:
|
||||
download:
|
||||
selector: a[href^="download.php?id="]
|
||||
attribute: href
|
||||
title:
|
||||
selector: a[href^="index.php?page=torrent-details&id="]
|
||||
attribute: title
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["Details anzeigen: ", ""]
|
||||
category:
|
||||
selector: a[href^="index.php?page=torrents&category="]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: category
|
||||
comments:
|
||||
selector: a[href*="#comments"]
|
||||
attribute: href
|
||||
size:
|
||||
selector: td:nth-child(3)
|
||||
grabs:
|
||||
selector: td:nth-child(5)
|
||||
filters:
|
||||
- name: split
|
||||
args: ["♦", 2]
|
||||
- name: replace
|
||||
args: ["---", "0"]
|
||||
seeders:
|
||||
selector: td:nth-child(5) > a:nth-child(1)
|
||||
leechers:
|
||||
selector: td:nth-child(5) > a:nth-child(2)
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
img[alt="gold"]: "0"
|
||||
img[alt="silver"]: "0.5"
|
||||
"*": "1"
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
img[alt="2x Upload Multiplier"]: "2"
|
||||
img[alt="3x Upload Multiplier"]: "3"
|
||||
img[alt="4x Upload Multiplier"]: "4"
|
||||
img[alt="5x Upload Multiplier"]: "5"
|
||||
img[alt="6x Upload Multiplier"]: "6"
|
||||
img[alt="7x Upload Multiplier"]: "7"
|
||||
img[alt="8x Upload Multiplier"]: "8"
|
||||
img[alt="9x Upload Multiplier"]: "9"
|
||||
img[alt="10x Upload Multiplier"]: "10"
|
||||
"*": "1"
|
||||
@@ -69,6 +69,8 @@
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}c{{.}}=1&{{end}}"
|
||||
search: "{{ .Keywords }}"
|
||||
sort: "id"
|
||||
order: "desc"
|
||||
incldead: "1"
|
||||
keywordsfilters:
|
||||
- name: replace
|
||||
@@ -110,4 +112,4 @@
|
||||
downloadvolumefactor:
|
||||
text: "0"
|
||||
uploadvolumefactor:
|
||||
text: "1"
|
||||
text: "1"
|
||||
|
||||
@@ -47,11 +47,41 @@
|
||||
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: ["(S1)", ""]
|
||||
- name: re_replace #remove S/EXX from search string
|
||||
args: ["(S2)", "segunda temporada"]
|
||||
- name: re_replace #remove S/EXX from search string
|
||||
args: ["(S3)", "tercera temporada"]
|
||||
- name: re_replace #remove S/EXX from search string
|
||||
args: ["E([0-9]+)", "$1"]
|
||||
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", ""]
|
||||
- name: re_replace
|
||||
args: ["S[pP]rimera", ""]
|
||||
- name: re_replace
|
||||
args: ["S[sS]egunda", ""]
|
||||
- name: re_replace
|
||||
args: ["S[tT]ercera", ""]
|
||||
- name: re_replace
|
||||
args: ["S([0-9]+) - Episodio ([0-9]+)", "$2"]
|
||||
- name: re_replace
|
||||
args: ["- Episodio ([0-9]*)", "$1"]
|
||||
- name: append
|
||||
args: " [spanish]"
|
||||
details:
|
||||
selector: td.tit a
|
||||
attribute: href
|
||||
|
||||
151
src/Jackett.Common/Definitions/gay-torrentsorg.yml
Normal file
151
src/Jackett.Common/Definitions/gay-torrentsorg.yml
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
# By LA5T for https://gay-torrents.org (29.07.2018 22:55 UTC+2)
|
||||
#
|
||||
site: gay-torrentsorg
|
||||
name: gay-torrents.org
|
||||
description: "Tracker for GAY XXX, movies, TV, books and PC."
|
||||
language: en-us
|
||||
type: semi-private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://gay-torrents.org
|
||||
- https://gay-area.org/
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: active
|
||||
type: select
|
||||
label: Status
|
||||
default: 0
|
||||
options:
|
||||
0: "Active and Inactive"
|
||||
1: Active
|
||||
2: Inactive
|
||||
- name: info_categories
|
||||
type: info
|
||||
label: "Hidden categories"
|
||||
default: "Results for categories hidden in profile will not be visible."
|
||||
- name: info_results
|
||||
type: info
|
||||
label: "Search results"
|
||||
default: "You can increase the number of search results in your profile.<br />Default is 15."
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 15, cat: XXX, desc: "Amateur"}
|
||||
- {id: 16, cat: XXX, desc: "Anal"}
|
||||
- {id: 42, cat: XXX, desc: "Animation"}
|
||||
- {id: 18, cat: XXX, desc: "Asian"}
|
||||
- {id: 19, cat: XXX, desc: "Bareback"}
|
||||
- {id: 20, cat: XXX, desc: "Bears"}
|
||||
- {id: 22, cat: XXX, desc: "Bisexual"}
|
||||
- {id: 21, cat: XXX, desc: "Black"}
|
||||
- {id: 23, cat: XXX, desc: "Chubs"}
|
||||
- {id: 25, cat: XXX, desc: "Cross Generation"}
|
||||
- {id: 51, cat: XXX, desc: "Doctor/Medical"}
|
||||
- {id: 27, cat: XXX, desc: "Fetish"}
|
||||
- {id: 28, cat: XXX, desc: "Group Sex"}
|
||||
- {id: 30, cat: XXX, desc: "Hunks"}
|
||||
- {id: 52, cat: XXX, desc: "Interracial"}
|
||||
- {id: 68, cat: XXX, desc: "Homo Erotic"}
|
||||
- {id: 68, cat: Movies, desc: "Homo Erotic"}
|
||||
- {id: 68, cat: TV, desc: "Homo Erotic"}
|
||||
- {id: 68, cat: Other, desc: "Homo Erotic"}
|
||||
- {id: 32, cat: XXX, desc: "Latino"}
|
||||
- {id: 50, cat: XXX, desc: "Middle Eastern"}
|
||||
- {id: 33, cat: XXX, desc: "Military"}
|
||||
- {id: 34, cat: XXX, desc: "Oral-Sex"}
|
||||
- {id: 40, cat: Other, desc: "Other"}
|
||||
- {id: 35, cat: XXX, desc: "Solo"}
|
||||
- {id: 36, cat: XXX, desc: "Transsexual"}
|
||||
- {id: 37, cat: XXX, desc: "Twinks"}
|
||||
- {id: 38, cat: XXX, desc: "Vintage"}
|
||||
- {id: 39, cat: XXX, desc: "Wrestling"}
|
||||
- {id: 17, cat: PC, desc: "Applications"}
|
||||
- {id: 31, cat: XXX/Imageset, desc: "Images"}
|
||||
- {id: 49, cat: Books, desc: "Books"}
|
||||
- {id: 41, cat: Movies, desc: "Non-Porn"}
|
||||
- {id: 41, cat: TV, desc: "Non-Porn"}
|
||||
- {id: 41, cat: Other, desc: "Non-Porn"}
|
||||
modes:
|
||||
search: [q]
|
||||
|
||||
login:
|
||||
method: post
|
||||
path: login.php
|
||||
inputs:
|
||||
uid: "{{ .Config.username }}"
|
||||
pwd: "{{ .Config.password }}"
|
||||
error:
|
||||
- selector: div.fixed_width:nth-child(4) > span:contains("Incorrect")
|
||||
test:
|
||||
path: usercp.php
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: torrents_beta.php
|
||||
method: get
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}category[]={{.}}&{{end}}"
|
||||
search: "{{ .Query.Keywords }}"
|
||||
active: "{{ .Config.active }}"
|
||||
rows:
|
||||
selector: div.torrent
|
||||
fields:
|
||||
title:
|
||||
selector: div:nth-child(2) > div:nth-child(1) > a:nth-child(1)
|
||||
category:
|
||||
selector: a[href^="torrents_beta.php?category="]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: category
|
||||
details:
|
||||
selector: a[href^="details.php?id="]
|
||||
attribute: href
|
||||
banner:
|
||||
optional: true
|
||||
selector: a.screenshot
|
||||
attribute: rel
|
||||
grabs:
|
||||
selector: div.downloadTimes
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["No downloads yet", "0"]
|
||||
- name: regexp
|
||||
args: ([\d,]+)
|
||||
comments:
|
||||
selector: a[href^="details.php?id="]
|
||||
attribute: href
|
||||
download:
|
||||
selector: a[href^="download.php?id="]
|
||||
attribute: href
|
||||
size:
|
||||
selector: div.size
|
||||
seeders:
|
||||
selector: div.downloadPeers div:nth-child(1) > a
|
||||
leechers:
|
||||
selector: div.downloadPeers > div:nth-child(2) > a
|
||||
date:
|
||||
selector: div.date
|
||||
filters:
|
||||
- name: re_replace
|
||||
args: ["on (\\d{2}:\\d{2}) (\\d{2})-([a-zA-Z]{3})-(\\d{4})", "$2 $3 $4 $1"]
|
||||
- name: append
|
||||
args: " +01:00"
|
||||
- name: dateparse
|
||||
args: "02 Jan 2006 15:04 -07:00"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"div:nth-child(2) > div:nth-child(3) > a:contains(\"FREE!\")": 0
|
||||
"div:nth-child(2) > div:nth-child(3) > a:contains(\"-50%\")": 0.5
|
||||
"div:nth-child(2) > div:nth-child(3) > a:contains(\"-25%\")": 0.25
|
||||
"*": 1
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
"*": 1
|
||||
@@ -1,52 +1,80 @@
|
||||
---
|
||||
# Update by LA5T based on the orignial 'gaytorrentru.yml'
|
||||
# 29.07.2018 23:02 UTC+2
|
||||
#
|
||||
site: gaytorrentru
|
||||
name: GayTorrent.ru
|
||||
description: "World largest gay porn library for free with a stunning forum and 24/7 Chat"
|
||||
name: GAYtorrent.ru
|
||||
description: "World largest gay porn library for free with a stunning forum and 24/7 chat."
|
||||
language: en-us
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://www.gaytorrent.ru/
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: incldead
|
||||
type: select
|
||||
label: Status
|
||||
default: 1
|
||||
options:
|
||||
0: Active
|
||||
1: "Active and Inactive"
|
||||
2: Inactive
|
||||
- name: info
|
||||
type: info
|
||||
label: "Hidden categories"
|
||||
default: "For results in the following categories you must edit your profile.<br />- Straight<br />- Bisexual<br />- Scat"
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 62 , cat: XXX , desc: "Amateur"}
|
||||
- {id: 29 , cat: XXX , desc: "Anal"}
|
||||
- {id: 46 , cat: XXX , desc: "Anime Games"}
|
||||
- {id: 30 , cat: XXX , desc: "Asian"}
|
||||
- {id: 43 , cat: XXX , desc: "Bareback"}
|
||||
- {id: 19 , cat: XXX , desc: "BDSM"}
|
||||
- {id: 17 , cat: XXX , desc: "Bears"}
|
||||
- {id: 44 , cat: XXX , desc: "Black"}
|
||||
- {id: 50 , cat: Books , desc: "Books & Magazines"}
|
||||
- {id: 9 , cat: XXX , desc: "Chubbies"}
|
||||
- {id: 7 , cat: XXX , desc: "Clips"}
|
||||
- {id: 48 , cat: Books/Comics , desc: "Comic & Yaoi"}
|
||||
- {id: 5 , cat: XXX , desc: "Daddies / Sons"}
|
||||
- {id: 34 , cat: XXX , desc: "Fetish"}
|
||||
- {id: 27 , cat: XXX , desc: "Grey / Older"}
|
||||
- {id: 32 , cat: XXX , desc: "Group-Sex"}
|
||||
- {id: 63 , cat: XXX , desc: "Homemade"}
|
||||
- {id: 12 , cat: XXX , desc: "Hunks"}
|
||||
- {id: 33 , cat: XXX , desc: "Images"}
|
||||
- {id: 53 , cat: XXX , desc: "Interracial"}
|
||||
- {id: 57 , cat: XXX , desc: "Jocks"}
|
||||
- {id: 35 , cat: XXX , desc: "Latino"}
|
||||
- {id: 36 , cat: XXX , desc: "Mature"}
|
||||
- {id: 58 , cat: PC , desc: "Media Programs"}
|
||||
- {id: 37 , cat: XXX , desc: "Member"}
|
||||
- {id: 54 , cat: XXX , desc: "Middle Eastern"}
|
||||
- {id: 38 , cat: XXX , desc: "Military"}
|
||||
- {id: 39 , cat: XXX , desc: "Oral-Sex"}
|
||||
- {id: 47 , cat: XXX , desc: "Shemale"}
|
||||
- {id: 56 , cat: XXX , desc: "Softcore"}
|
||||
- {id: 40 , cat: XXX , desc: "Solo"}
|
||||
- {id: 45 , cat: Movies , desc: "Themed Movie"}
|
||||
- {id: 1 , cat: TV , desc: "TV / Episodes"}
|
||||
- {id: 41 , cat: XXX , desc: "Twinks"}
|
||||
- {id: 42 , cat: XXX , desc: "Vintage"}
|
||||
- {id: 51 , cat: XXX , desc: "Voyeur"}
|
||||
- {id: 65 , cat: XXX , desc: "Wrestling and Sports"}
|
||||
- {id: 28 , cat: XXX , desc: "Youngblood"}
|
||||
- {id: 62, cat: XXX, desc: "Amateur"}
|
||||
- {id: 29, cat: XXX, desc: "Anal"}
|
||||
- {id: 46, cat: XXX, desc: "Anime Games"}
|
||||
- {id: 30, cat: XXX, desc: "Asian"}
|
||||
- {id: 43, cat: XXX, desc: "Bareback"}
|
||||
- {id: 19, cat: XXX, desc: "BDSM"}
|
||||
- {id: 17, cat: XXX, desc: "Bears"}
|
||||
- {id: 44, cat: XXX, desc: "Black"}
|
||||
- {id: 50, cat: Books, desc: "Books & Magazines"}
|
||||
- {id: 9, cat: XXX, desc: "Chubbies"}
|
||||
- {id: 7, cat: XXX, desc: "Clips"}
|
||||
- {id: 48, cat: Books/Comics, desc: "Comic & Yaoi"}
|
||||
- {id: 5, cat: XXX, desc: "Daddies / Sons"}
|
||||
- {id: 34, cat: XXX, desc: "Fetish"}
|
||||
- {id: 27, cat: XXX, desc: "Grey / Older"}
|
||||
- {id: 32, cat: XXX, desc: "Group-Sex"}
|
||||
- {id: 63, cat: XXX, desc: "Homemade"}
|
||||
- {id: 12, cat: XXX, desc: "Hunks"}
|
||||
- {id: 33, cat: XXX/Imageset, desc: "Images"}
|
||||
- {id: 53, cat: XXX, desc: "Interracial"}
|
||||
- {id: 57, cat: XXX, desc: "Jocks"}
|
||||
- {id: 35, cat: XXX, desc: "Latino"}
|
||||
- {id: 36, cat: XXX, desc: "Mature"}
|
||||
- {id: 58, cat: PC, desc: "Media Programs"}
|
||||
- {id: 37, cat: XXX, desc: "Member"}
|
||||
- {id: 54, cat: XXX, desc: "Middle Eastern"}
|
||||
- {id: 38, cat: XXX, desc: "Military"}
|
||||
- {id: 39, cat: XXX, desc: "Oral-Sex"}
|
||||
- {id: 47, cat: XXX, desc: "Shemale"}
|
||||
- {id: 56, cat: XXX, desc: "Softcore"}
|
||||
- {id: 40, cat: XXX, desc: "Solo"}
|
||||
- {id: 45, cat: Movies, desc: "Themed Movie"}
|
||||
- {id: 1, cat: TV, desc: "TV / Episodes"}
|
||||
- {id: 41, cat: XXX, desc: "Twinks"}
|
||||
- {id: 42, cat: XXX, desc: "Vintage"}
|
||||
- {id: 51, cat: XXX, desc: "Voyeur"}
|
||||
- {id: 65, cat: XXX, desc: "Wrestling and Sports"}
|
||||
- {id: 28, cat: XXX, desc: "Youngblood"}
|
||||
- {id: 59, cat: XXX, desc: "Bisexual"}
|
||||
- {id: 61, cat: XXX, desc: "Straight older"}
|
||||
- {id: 60, cat: XXX, desc: "Straight younger"}
|
||||
- {id: 64, cat: XXX, desc: "Scat"}
|
||||
modes:
|
||||
search: [q]
|
||||
|
||||
@@ -65,7 +93,7 @@
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}c{{.}}=1&{{end}}"
|
||||
search: "{{ .Query.Keywords }}"
|
||||
incldead: "0" # Searches only for alive torrents
|
||||
incldead: "{{ .Config.incldead }}"
|
||||
rows:
|
||||
selector: table.browse_result > tbody > tr:has(a[href^="details.php?id="])
|
||||
fields:
|
||||
@@ -89,12 +117,12 @@
|
||||
selector: .tfiles
|
||||
filters:
|
||||
- name: regexp
|
||||
args: ([\d]+)
|
||||
args: ([\d,]+)
|
||||
size:
|
||||
selector: .tsize
|
||||
seeders:
|
||||
optional: true
|
||||
selector: a[href$="&toseeders=1"]
|
||||
selector: a[href$="&toseeders=1"], span.red
|
||||
leechers:
|
||||
optional: true
|
||||
selector: a[href$="&todlers=1"]
|
||||
@@ -102,13 +130,13 @@
|
||||
selector: .tadded
|
||||
filters:
|
||||
- name: re_replace
|
||||
args: ["(\\d{4}-\\d{2}-\\d{2})(\\d{2}:\\d{2}:\\d{2})(.*)","$1 $2"]
|
||||
args: ["(\\d{4}-\\d{2}-\\d{2})(\\d{2}:\\d{2}:\\d{2}).*", "$1 $2"]
|
||||
- name: dateparse
|
||||
args: "2006-01-02 15:04:05"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"td:nth-child(3) > div > nobr > font[color=\"yellow\"]": "0"
|
||||
"*": "1"
|
||||
"td:nth-child(3) > div > nobr > font[color=\"yellow\"]": 0
|
||||
"*": 1
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
"*": "1"
|
||||
"*": 1
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
search:
|
||||
paths:
|
||||
- path: /torrents.php
|
||||
method: post
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}cat{{.}}=1&{{end}}"
|
||||
search: "{{if .Query.IMDBID}}{{ .Query.IMDBID }}{{else}}{{ .Keywords }}{{end}}"
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
search:
|
||||
paths:
|
||||
- path: /torrents.php
|
||||
method: post
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}cat{{.}}=1&{{end}}"
|
||||
search: "{{if .Query.IMDBID}}{{ .Query.IMDBID }}{{else}}{{ .Keywords }}{{end}}"
|
||||
|
||||
142
src/Jackett.Common/Definitions/hdspain.yml
Normal file
142
src/Jackett.Common/Definitions/hdspain.yml
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
site: hdspain
|
||||
name: HD-Spain
|
||||
description: "HD-Spain is a SPANISH site for HD content"
|
||||
language: es-es
|
||||
type: private
|
||||
encoding: ISO-8859-1
|
||||
links:
|
||||
- https://www.hd-spain.com/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 1 , cat: Movies/HD, desc: "Películas"}
|
||||
- {id: 5 , cat: Movies/HD, desc: "Pelíc. Anim."}
|
||||
- {id: 4 , cat: TV/HD, desc: "Series"}
|
||||
- {id: 3 , cat: TV/HD, desc: "Series Anim."}
|
||||
- {id: 6 , cat: TV/Documentary, desc: "Documentales"}
|
||||
- {id: 11, cat: TV/Sport, desc: "Deportes"}
|
||||
- {id: 7 , cat: Audio/Video, desc: "Música/Espec."}
|
||||
- {id: 9 , cat: TV/OTHER, desc: "Programas TV"}
|
||||
- {id: 8 , cat: Audio/Lossless, desc: "Audios"}
|
||||
- {id: 10, cat: XXX/x264, desc: "XXX"}
|
||||
modes:
|
||||
search: [q]
|
||||
|
||||
login:
|
||||
path: index.php
|
||||
method: form
|
||||
inputs:
|
||||
usuario: "{{ .Config.username }}"
|
||||
contrasena: "{{ .Config.password }}"
|
||||
error:
|
||||
- selector: p.error
|
||||
test:
|
||||
path: index.php
|
||||
selector: .tcabecera
|
||||
|
||||
search:
|
||||
path: index.php
|
||||
keywordsfilters:
|
||||
- name: re_replace
|
||||
args: ["S0?(\\d{1,2})E(\\d{1,2})", "$1x$2"]
|
||||
inputs:
|
||||
sec: listado
|
||||
ord: 9
|
||||
b: "{{ .Keywords }}"
|
||||
ver: "0"
|
||||
relanz: "0"
|
||||
$raw: "{{range .Categories}}&cat[]={{.}}{{end}}"
|
||||
rows:
|
||||
selector: "table.listatorrents tr:not(:first-child)"
|
||||
fields:
|
||||
category:
|
||||
selector: td.categorias a
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: cat
|
||||
title:
|
||||
selector: td.titulo a[id]
|
||||
filters:
|
||||
- name: append
|
||||
args: " [spanish]"
|
||||
details:
|
||||
selector: td.titulo a
|
||||
attribute: href
|
||||
size:
|
||||
selector: td.tamano
|
||||
seeders:
|
||||
selector: td.usuarios.seeds a
|
||||
leechers:
|
||||
selector: td.usuarios.leechers a
|
||||
grabs:
|
||||
selector: td.usuarios.completados
|
||||
date:
|
||||
optional: true
|
||||
selector: td.fecha
|
||||
attribute: title
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["Lunes", "Monday"]
|
||||
- name: replace
|
||||
args: ["Martes", "Tuesday"]
|
||||
- name: re_replace
|
||||
args: ["Miércoles", "Wednesday"]
|
||||
- name: replace
|
||||
args: ["Jueves", "Thursday"]
|
||||
- name: replace
|
||||
args: ["Viernes", "Friday"]
|
||||
- name: re_replace
|
||||
args: ["Sábado", "Saturday"]
|
||||
- name: replace
|
||||
args: ["Domingo", "Sunday"]
|
||||
- name: replace
|
||||
args: ["Enero", "January"]
|
||||
- name: replace
|
||||
args: ["Febrero", "February"]
|
||||
- name: replace
|
||||
args: ["Marzo", "March"]
|
||||
- name: replace
|
||||
args: ["Abril", "April"]
|
||||
- name: replace
|
||||
args: ["Mayo", "May"]
|
||||
- name: replace
|
||||
args: ["Junio", "June"]
|
||||
- name: replace
|
||||
args: ["Julio", "July"]
|
||||
- name: replace
|
||||
args: ["Agosto", "August"]
|
||||
- name: replace
|
||||
args: ["Septiembre", "September"]
|
||||
- name: replace
|
||||
args: ["Octubre", "October"]
|
||||
- name: replace
|
||||
args: ["Noviembre", "November"]
|
||||
- name: replace
|
||||
args: ["Diciembre", "December"]
|
||||
- name: dateparse
|
||||
args: "Monday 2 January 2006, 15:04"
|
||||
download:
|
||||
selector: td.descargar a
|
||||
attribute: href
|
||||
|
||||
downloadvolumefactor:
|
||||
text: "1"
|
||||
downloadvolumefactor:
|
||||
optional: true
|
||||
selector: td.descargar a b strong
|
||||
filters:
|
||||
- name: replace
|
||||
args: [" X2", ""]
|
||||
- name: replace
|
||||
args: ["Freeleech", "0"]
|
||||
|
||||
uploadvolumefactor:
|
||||
text: "1"
|
||||
uploadvolumefactor:
|
||||
optional: true
|
||||
selector: td.descargar a b strong
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["Freeleech X2", "2"]
|
||||
@@ -6,10 +6,12 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://www.ilcorsaroblu.info/
|
||||
- https://www.ilcorsaroblu.org/
|
||||
legacylinks:
|
||||
- http://ilcorsaroblu.org/
|
||||
- https://www.ilcorsaroblu.org/
|
||||
- https://www.ilcorsaroblu.info/
|
||||
certificates:
|
||||
- e6dd93ef71f96d04559e2bee8cc8e6fd74957730 # incomplete CA chain
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
@@ -143,18 +145,18 @@
|
||||
- name: regexp
|
||||
args: "src=(.+?) "
|
||||
size:
|
||||
selector: td:nth-child(9)
|
||||
selector: td:nth-child(10)
|
||||
date:
|
||||
selector: td:nth-child(5)
|
||||
selector: td:nth-child(6)
|
||||
filters:
|
||||
- name: dateparse
|
||||
args: "02/01/2006"
|
||||
grabs:
|
||||
selector: td:nth-child(8)
|
||||
selector: td:nth-child(9)
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["---", "0"]
|
||||
seeders:
|
||||
selector: td:nth-child(6)
|
||||
leechers:
|
||||
selector: td:nth-child(7)
|
||||
leechers:
|
||||
selector: td:nth-child(8)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- https://ilcorsaronero.info/
|
||||
certificates:
|
||||
- aa7c40aa360a1cec8a9687312fd50402b912e618 # incomplete CA chain
|
||||
- 83174ec1f92fa13cdef9d51888ea1dfba2166e17 # incomplete CA chain
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
|
||||
@@ -84,11 +84,12 @@
|
||||
selector: table#torrents_table_classic > tbody > tr:not(:first-child), div#content > div.torrent-box[id^="torrent_"]
|
||||
filters:
|
||||
- name: andmatch
|
||||
# the tracker has two different styles (modern/classic) it should work with both
|
||||
fields:
|
||||
title:
|
||||
selector: a[href*="?p=torrents&pid=10&action=details"]
|
||||
category:
|
||||
selector: div.category_image > a
|
||||
selector: div.category_image > a, div.categoryImage > a
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
@@ -117,6 +118,9 @@
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"img[title=\"ΧΡΥΣΟ!\"]": "0"
|
||||
"img[title=\"100% FREE!\"]": "0"
|
||||
"img[title=\"Πολλαπλασιαστής Κατεβάσματος: 0.5\"]": "0.5"
|
||||
|
||||
"*": "1"
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
---
|
||||
# Update by LA5T based on the orignial 'karagarga.yml'
|
||||
# 29.07.2018 23:15 UTC+2
|
||||
#
|
||||
site: karagarga
|
||||
name: Karagarga
|
||||
description: "Rare and obscure movie tracker"
|
||||
description: "Tracker for non-hollywood, rare and obscure movies, music and literature."
|
||||
language: en-us
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
@@ -11,6 +14,66 @@
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 1, cat: Movies, desc: "Movies"}
|
||||
- {id: 2, cat: Audio, desc: "Music"}
|
||||
- {id: 3, cat: Books, desc: "Literature"}
|
||||
- {id: 4, cat: Movies, desc: "Action"}
|
||||
- {id: 55, cat: Movies, desc: "Adventure"}
|
||||
- {id: 5, cat: Movies, desc: "Animation"}
|
||||
- {id: 6, cat: Movies, desc: "Arthouse"}
|
||||
- {id: 7, cat: Movies, desc: "Asian"}
|
||||
- {id: 43, cat: Movies, desc: "Camp"}
|
||||
- {id: 8, cat: Movies, desc: "Classics"}
|
||||
- {id: 9, cat: Movies, desc: "Comedy"}
|
||||
- {id: 10, cat: Movies, desc: "Crime"}
|
||||
- {id: 11, cat: Movies, desc: "Cult"}
|
||||
- {id: 20, cat: Movies, desc: "Documentary"}
|
||||
- {id: 12, cat: Movies, desc: "Drama"}
|
||||
- {id: 44, cat: Movies, desc: "Epic"}
|
||||
- {id: 13, cat: Movies, desc: "Erotica"}
|
||||
- {id: 51, cat: Movies, desc: "Experimental"}
|
||||
- {id: 47, cat: Movies, desc: "Exploitation"}
|
||||
- {id: 14, cat: Movies, desc: "Fantasy"}
|
||||
- {id: 15, cat: Movies, desc: "Film Noir"}
|
||||
- {id: 53, cat: Movies, desc: "Giallo"}
|
||||
- {id: 17, cat: Movies, desc: "Horror"}
|
||||
- {id: 18, cat: Movies, desc: "Martial Arts"}
|
||||
- {id: 19, cat: Movies, desc: "Musical"}
|
||||
- {id: 54, cat: Movies, desc: "Mystery"}
|
||||
- {id: 60, cat: Movies, desc: "Performance"}
|
||||
- {id: 48, cat: Movies, desc: "Philosophy"}
|
||||
- {id: 49, cat: Movies, desc: "Politics"}
|
||||
- {id: 50, cat: Movies, desc: "Romance"}
|
||||
- {id: 21, cat: Movies, desc: "Sci-Fi"}
|
||||
- {id: 22, cat: Movies, desc: "Short"}
|
||||
- {id: 23, cat: Movies, desc: "Silent"}
|
||||
- {id: 24, cat: Movies, desc: "Thriller"}
|
||||
- {id: 25, cat: Movies, desc: "TV"}
|
||||
- {id: 56, cat: Movies, desc: "Video Art"}
|
||||
- {id: 26, cat: Movies, desc: "War"}
|
||||
- {id: 27, cat: Movies, desc: "Western"}
|
||||
- {id: 800, cat: Audio, desc: "Blues"}
|
||||
- {id: 31, cat: Audio, desc: "Classical"}
|
||||
- {id: 600, cat: Audio, desc: "Country"}
|
||||
- {id: 34, cat: Audio, desc: "Electronica"}
|
||||
- {id: 63, cat: Audio, desc: "Exotica"}
|
||||
- {id: 1000, cat: Audio, desc: "Experimental"}
|
||||
- {id: 250, cat: Audio, desc: "Folk"}
|
||||
- {id: 500, cat: Audio, desc: "Funk"}
|
||||
- {id: 700, cat: Audio, desc: "Indie"}
|
||||
- {id: 32, cat: Audio, desc: "Jazz"}
|
||||
- {id: 1200, cat: Audio, desc: "Latin"}
|
||||
- {id: 35, cat: Audio, desc: "Live"}
|
||||
- {id: 900, cat: Audio, desc: "Metal"}
|
||||
- {id: 62, cat: Audio, desc: "Punk & Hardcore"}
|
||||
- {id: 52, cat: Audio, desc: "Rap & Hiphop"}
|
||||
- {id: 5000, cat: Audio, desc: "Reggae"}
|
||||
- {id: 36, cat: Audio, desc: "Rock"}
|
||||
- {id: 400, cat: Audio, desc: "Soul"}
|
||||
- {id: 33, cat: Audio, desc: "Soundtrack"}
|
||||
- {id: 30, cat: Audio, desc: "World"}
|
||||
- {id: 40, cat: Audio/Audiobook, desc: "Audiobooks"}
|
||||
- {id: 41, cat: Books, desc: "Books"}
|
||||
- {id: 42, cat: Books/Comics, desc: "Comics"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
@@ -33,15 +96,22 @@
|
||||
search:
|
||||
paths:
|
||||
- path: browse.php
|
||||
keywordsfilters:
|
||||
- name: re_replace
|
||||
args: ["(?<=^| )(?!-|\\+)[^ ]+(?= |$)", "+$&"]
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}filter_cat[{{.}}]=1&{{end}}"
|
||||
search: "\"{{ .Query.Keywords }}\""
|
||||
search_type: "title"
|
||||
$raw: "{{range .Categories}}genre={{.}}&{{end}}"
|
||||
search: "{{ .Keywords }}"
|
||||
search_type: title
|
||||
rows:
|
||||
selector: table#browse > tbody > tr:has(a[href^="browse.php?genre="])
|
||||
fields:
|
||||
category:
|
||||
text: 1
|
||||
selector: a[href^="browse.php?genre="]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: genre
|
||||
title:
|
||||
selector: td:nth-child(2) span
|
||||
download:
|
||||
@@ -69,15 +139,16 @@
|
||||
date:
|
||||
selector: td:nth-child(9)
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["'", ""]
|
||||
- name: re_replace
|
||||
args: ["[^a-zA-Z0-9]+", " "]
|
||||
args: ["([a-zA-Z]+)\\s+(\\d{1,2})\\s+'(\\d{2})", "$2 $1 $3"]
|
||||
- name: dateparse
|
||||
args: "Jan 02 06"
|
||||
args: "02 Jan 06"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"*": "1"
|
||||
"*": 1
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
"*": "1"
|
||||
":has(img[title^=HD]):has(img[title^=CURRENT])": 1.8
|
||||
"img[title^=CURRENT]": 1.6
|
||||
"img[title^=HD]": 1.3
|
||||
"*": 1.1
|
||||
@@ -6,6 +6,8 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://www.limetorrents.io/
|
||||
legacylinks:
|
||||
- https://www.limetorrents.cc/
|
||||
|
||||
caps:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
site: nexttorrent
|
||||
name: NextTorrent
|
||||
description: "NextTorrent is a FRENCH Public site for TV / MOVIES / GENERAL"
|
||||
language: fr-fr
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://www.nextorrent.tv/
|
||||
legacylinks:
|
||||
- https://www.nextorrent.site/
|
||||
- http://www.nextorrent.site/
|
||||
- http://www.nextorrent.bz/
|
||||
- http://www.nextorrent.pro/
|
||||
- https://www.nextorrent.cc/
|
||||
- https://www.nextorrent.org/
|
||||
- https://www.nextorrent.tv/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: Films, cat: Movies, desc: "Movies"}
|
||||
- {id: Séries, cat: TV, desc: "TV"}
|
||||
- {id: Jeux-PC, cat: PC/Games, desc: "Games PC"}
|
||||
- {id: Jeux-Consoles, cat: Console, desc: "Games Console"}
|
||||
- {id: Musique, cat: Audio, desc: "Music"}
|
||||
- {id: Ebook, cat: Books, desc: "EBooks"}
|
||||
- {id: Logiciels, cat: PC, desc: "Software"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q, season, ep]
|
||||
movie-search: [q]
|
||||
|
||||
settings: []
|
||||
|
||||
download:
|
||||
selector: a[href^="/get_torrent/"]
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: "recherche/{{ .Query.Keywords }}"
|
||||
rows:
|
||||
selector: div.listing-torrent > table tbody tr:has(a)
|
||||
fields:
|
||||
site_date:
|
||||
selector: td:nth-child(1) a
|
||||
filters:
|
||||
# date is at the end of the title, so we get it and name it site_date
|
||||
- name: regexp
|
||||
args: "(\\w+)$"
|
||||
title:
|
||||
selector: td:nth-child(1) a
|
||||
filters:
|
||||
# now we put the date at the right place according scene naming rules using .Result.site_date
|
||||
- name: replace
|
||||
args: ["FRENCH", "{{ .Result.site_date }} FRENCH"]
|
||||
- name: replace
|
||||
args: ["TRUEFRENCH", "{{ .Result.site_date }} TRUEFRENCH"]
|
||||
- name: replace
|
||||
args: ["VOSTFR", "{{ .Result.site_date }} VOSTFR"]
|
||||
# and we delete it at the end
|
||||
- name: re_replace
|
||||
args: ["(\\w+)$", ""]
|
||||
details:
|
||||
selector: td:nth-child(1) a
|
||||
attribute: href
|
||||
download:
|
||||
selector: td:nth-child(1) a
|
||||
attribute: href
|
||||
category:
|
||||
selector: td:nth-child(1) i
|
||||
attribute: class
|
||||
size:
|
||||
selector: td:nth-child(2)
|
||||
date:
|
||||
text: now
|
||||
seeders:
|
||||
text: 0
|
||||
seeders:
|
||||
optional: true
|
||||
selector: td:nth-child(3)
|
||||
leechers:
|
||||
text: 0
|
||||
leechers:
|
||||
optional: true
|
||||
selector: td:nth-child(4)
|
||||
downloadvolumefactor:
|
||||
text: "0"
|
||||
uploadvolumefactor:
|
||||
text: "1"
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://v2.rgut.uk/
|
||||
legacylinks:
|
||||
- https://rgu.rgt.life/
|
||||
|
||||
caps:
|
||||
@@ -65,8 +67,7 @@
|
||||
inputs:
|
||||
username: "{{ .Config.username }}"
|
||||
password: "{{ .Config.password }}"
|
||||
submitme: "X"
|
||||
submitme: "X" # two submitme needed?!? shouldn't make a difference
|
||||
use_ssl: "1"
|
||||
error:
|
||||
- selector: h2:contains("Login failed!")
|
||||
message:
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://shareisland.org/
|
||||
- https://shareisland.org/
|
||||
legacylinks:
|
||||
- http://shareisland.org/
|
||||
- http://www.shareisland.org/
|
||||
|
||||
caps:
|
||||
@@ -30,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,13 +6,15 @@
|
||||
type: public
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- http://www.torrent9.ec/
|
||||
- https://www.torrent9.blue/
|
||||
legacylinks:
|
||||
- http://www.torrent9.ec/
|
||||
- http://www.torrent9.red/
|
||||
- http://www.torrent9.bz/
|
||||
- http://www.torrents9.pe/
|
||||
- http://www.torrent9.cc/
|
||||
- http://www.torrent9.pe/
|
||||
- http://www.torrent9.blue/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
site: torrentwtf
|
||||
name: Torrentwtf
|
||||
description: "Torrentwtf is a Czech Private site for TV / MOVIES / GENERAL"
|
||||
language: cs-cz
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://torrent.wtf/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 1, cat: Movies, desc: "Filmy"}
|
||||
- {id: 2, cat: TV, desc: "Seriály"}
|
||||
- {id: 3, cat: Audio, desc: "Hudba"}
|
||||
- {id: 5, cat: PC/Games, desc: "Hry"}
|
||||
- {id: 6, cat: Books, desc: "Knihy"}
|
||||
- {id: 8, cat: PC, desc: "Software"}
|
||||
- {id: 9, cat: XXX, desc: "xXx"}
|
||||
- {id: 10, cat: Other, desc: "Ostatní"}
|
||||
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q, season, ep, imdbid]
|
||||
movie-search: [q, imdbid]
|
||||
|
||||
login:
|
||||
path: /login
|
||||
method: form
|
||||
inputs:
|
||||
username: "{{ .Config.username }}"
|
||||
password: "{{ .Config.password }}"
|
||||
error:
|
||||
- selector: table.main:contains("Tieto poverenia sa nezhodujú s našimi záznamami.")
|
||||
test:
|
||||
path: /torrents
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: /filter
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}categories[]={{.}}&{{end}}"
|
||||
search: "{{if .Query.IMDBID}}{{else}}{{ .Keywords }}{{end}}"
|
||||
imdb: "{{ .Query.IMDBIDShort }}"
|
||||
tvdb: ""
|
||||
tmdb: ""
|
||||
sorting: created_at
|
||||
direction: desc
|
||||
qty: 100
|
||||
preprocessingfilters:
|
||||
- name: jsonjoinarray
|
||||
args: ["$.result", ""]
|
||||
- name: prepend
|
||||
args: "<table>"
|
||||
- name: append
|
||||
args: "</table>"
|
||||
rows:
|
||||
selector: tr
|
||||
fields:
|
||||
category:
|
||||
selector: a[href*="/categories/"]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: regexp
|
||||
args: "/categories/.*?\\.(\\d+)"
|
||||
title:
|
||||
selector: a.view-torrent
|
||||
filters:
|
||||
- name: re_replace
|
||||
args: [".*? / ", ""]
|
||||
download:
|
||||
selector: a[href*="/download_check/"]
|
||||
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)
|
||||
seeders:
|
||||
selector: td:nth-child(7)
|
||||
leechers:
|
||||
selector: td:nth-child(8)
|
||||
grabs:
|
||||
selector: td:nth-child(6)
|
||||
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"
|
||||
"i[data-original-title=\"Global FreeLeech\"]": "0"
|
||||
"*": "1"
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
"i[data-original-title=\"Double upload\"]": "2"
|
||||
"*": "1"
|
||||
@@ -120,7 +120,7 @@
|
||||
selector: table#browsetable > tbody > tr:has(a[href^="/details.php?id="])
|
||||
fields:
|
||||
category:
|
||||
selector: a[href^="/browse.php?q="]
|
||||
selector: a[href^="/browse.php"]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
165
src/Jackett.Common/Definitions/x-ite.me.yml
Normal file
165
src/Jackett.Common/Definitions/x-ite.me.yml
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
# By LA5T for https://x-ite.me (25.07.2018 17:34 UTC+2)
|
||||
#
|
||||
# TODO:
|
||||
# x-ite.me doesn't support altering the number of results, therefore only the 20 first results are shown (this is hardcoded), Jackett may support pagination in the future tho
|
||||
#
|
||||
site: xiteme
|
||||
name: x-ite.me
|
||||
description: "Tracker for LGBTQ movies, TV, books, magazines, anime, PC and XXX."
|
||||
language: en-us
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://x-ite.me
|
||||
|
||||
settings:
|
||||
- name: username
|
||||
type: text
|
||||
label: Username
|
||||
- name: password
|
||||
type: password
|
||||
label: Password
|
||||
- name: incldead
|
||||
type: select
|
||||
label: Status
|
||||
default: 1
|
||||
options:
|
||||
0: Active
|
||||
1: "Active and Inactive"
|
||||
2: Inactive
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 6700, cat: TV/Anime, desc: "Animations - Adult"}
|
||||
- {id: 6330, cat: TV/Anime, desc: "Animations - Futanari"}
|
||||
- {id: 6320, cat: TV/Anime, desc: "Animations - Yaoi"}
|
||||
- {id: 9700, cat: Books/Comics, desc: "Comics & Manga - Adult"}
|
||||
- {id: 5000, cat: XXX, desc: "Fetish - All"}
|
||||
- {id: 7700, cat: XXX/Imageset, desc: "Images - Fetish"}
|
||||
- {id: 1050, cat: Movies, desc: "Movies - Biography"}
|
||||
- {id: 1150, cat: Movies, desc: "Movies - Crime"}
|
||||
- {id: 1250, cat: Movies, desc: "Movies - Experimental"}
|
||||
- {id: 1350, cat: Movies, desc: "Movies - History"}
|
||||
- {id: 1450, cat: Movies, desc: "Movies - Mystery"}
|
||||
- {id: 1530, cat: Movies, desc: "Movies - Sport"}
|
||||
- {id: 1630, cat: Movies, desc: "Movies - Western"}
|
||||
- {id: 6100, cat: TV/Anime, desc: "Animations - Anime"}
|
||||
- {id: 6300, cat: TV/Anime, desc: "Animations - Hentai"}
|
||||
- {id: 6310, cat: TV/Anime, desc: "Animations - Yuri"}
|
||||
- {id: 9600, cat: Books/Comics, desc: "Comics & Manga - Cartoon"}
|
||||
- {id: 13000, cat: PC/Games, desc: "Games - All"}
|
||||
- {id: 7900, cat: XXX/Imageset, desc: "Images - Other"}
|
||||
- {id: 1070, cat: Movies, desc: "Movies - Bollywood"}
|
||||
- {id: 1170, cat: Movies, desc: "Movies - Documentary"}
|
||||
- {id: 1270, cat: Movies, desc: "Movies - Family"}
|
||||
- {id: 1370, cat: Movies, desc: "Movies - Horror"}
|
||||
- {id: 1470, cat: Movies, desc: "Movies - News"}
|
||||
- {id: 1550, cat: Movies, desc: "Movies - Suspense"}
|
||||
- {id: 3000, cat: XXX, desc: "Softcore - All"}
|
||||
- {id: 6340, cat: TV/Anime, desc: "Animations - Bara"}
|
||||
- {id: 6900, cat: TV/Anime, desc: "Animations - Other"}
|
||||
- {id: 14000, cat: PC, desc: "Applications - All"}
|
||||
- {id: 9200, cat: Books/Comics, desc: "Comics & Manga - Hentai"}
|
||||
- {id: 4000, cat: XXX, desc: "Hardcore - All"}
|
||||
- {id: 8000, cat: Books/Magazines, desc: "Magazines - All"}
|
||||
- {id: 1090, cat: Movies, desc: "Movies - Comedy"}
|
||||
- {id: 1190, cat: Movies, desc: "Movies - Drama"}
|
||||
- {id: 1290, cat: Movies, desc: "Movies - Fantasy"}
|
||||
- {id: 1390, cat: Movies, desc: "Movies - Indie"}
|
||||
- {id: 1900, cat: Movies, desc: "Movies - Other"}
|
||||
- {id: 1570, cat: Movies, desc: "Movies - Theater"}
|
||||
- {id: 12000, cat: Other, desc: "Subtitles - All"}
|
||||
- {id: 6110, cat: TV/Anime, desc: "Animations - Ecchi"}
|
||||
- {id: 6120, cat: TV/Anime, desc: "Animations - Shoujo Ai"}
|
||||
- {id: 11000, cat: Audio/Audiobook, desc: "Audio Books - All"}
|
||||
- {id: 9100, cat: Books/Comics, desc: "Comics & Manga - Manga"}
|
||||
- {id: 7500, cat: XXX/Imageset, desc: "Images - Adult"}
|
||||
- {id: 1010, cat: Movies, desc: "Movies - Action"}
|
||||
- {id: 1110, cat: Movies, desc: "Movies - Coming of Age"}
|
||||
- {id: 1210, cat: Movies, desc: "Movies - Eastern"}
|
||||
- {id: 1310, cat: Movies, desc: "Movies - Film Noir"}
|
||||
- {id: 1410, cat: Movies, desc: "Movies - Music"}
|
||||
- {id: 1490, cat: Movies, desc: "Movies - Romance"}
|
||||
- {id: 1590, cat: Movies, desc: "Movies - Thriller"}
|
||||
- {id: 6350, cat: TV/Anime, desc: "Animations - Furry"}
|
||||
- {id: 6330, cat: TV/Anime, desc: "Animations - Shounen Ai"}
|
||||
- {id: 10000, cat: Books, desc: "Books - All"}
|
||||
- {id: 9900, cat: Books/Comics, desc: "Comics & Manga - Other"}
|
||||
- {id: 7300, cat: XXX/Imageset, desc: "Images - Erotic"}
|
||||
- {id: 1030, cat: Movies, desc: "Movies - Adventure"}
|
||||
- {id: 1130, cat: Movies, desc: "Movies - Coming Out"}
|
||||
- {id: 1230, cat: Movies, desc: "Movies - Entertainment"}
|
||||
- {id: 1330, cat: Movies, desc: "Movies - Historical"}
|
||||
- {id: 1430, cat: Movies, desc: "Movies - Musical"}
|
||||
- {id: 1510, cat: Movies, desc: "Movies - Sci-Fi"}
|
||||
- {id: 1610, cat: Movies, desc: "Movies - War"}
|
||||
modes:
|
||||
search: [q]
|
||||
|
||||
login:
|
||||
method: post
|
||||
path: account-login.php
|
||||
inputs:
|
||||
username: "{{ .Config.username }}"
|
||||
password: "{{ .Config.password }}"
|
||||
error:
|
||||
- selector: .myF-content > center:nth-child(1) > b:contains("The specified username or password was incorrect.")
|
||||
test:
|
||||
path: account.php
|
||||
|
||||
search:
|
||||
paths:
|
||||
- path: torrents-search.php
|
||||
method: get
|
||||
keywordsfilters:
|
||||
- name: re_replace
|
||||
args: ["(?<=^| )(?!-|\\+)[^ ]+(?= |$)", "+$&"]
|
||||
inputs:
|
||||
$raw: "{{range .Categories}}&c{{.}}=1&{{end}}"
|
||||
search: "{{ .Keywords }}"
|
||||
incldead: "{{ .Config.incldead }}"
|
||||
rows:
|
||||
selector: tr.t-row
|
||||
fields:
|
||||
title:
|
||||
selector: td:nth-child(2) > a:nth-child(1) > b:nth-child(1)
|
||||
category:
|
||||
selector: a[href^="torrents.php?cat="]
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: cat
|
||||
details:
|
||||
selector: a[href^="torrents-details.php?id="]
|
||||
attribute: href
|
||||
comments:
|
||||
selector: a[href^="comments.php?type=torrent&id="]
|
||||
attribute: href
|
||||
download:
|
||||
selector: a[href^="download.php?id="]
|
||||
attribute: href
|
||||
size:
|
||||
selector: td:nth-child(7)
|
||||
files:
|
||||
selector: td:nth-child(6)
|
||||
seeders:
|
||||
selector: td:nth-child(8)
|
||||
leechers:
|
||||
selector: td:nth-child(9)
|
||||
date:
|
||||
selector: td:nth-child(3)
|
||||
filters:
|
||||
- name: re_replace
|
||||
args: ["(\\d{2})-(\\d{2})-(\\d{4}) ((?:\\d{2}:?){3})", "$3-$2-$1 $4"]
|
||||
- name: append
|
||||
args: " +01:00"
|
||||
- name: dateparse
|
||||
args: "2006-01-02 15:04:05 -07:00"
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
"td:nth-child(2) > a:nth-child(1) > span:nth-child(2)": 0
|
||||
"*": 1
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
"*": 1
|
||||
@@ -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:
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
type: semi-private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://yggtorrent.is/
|
||||
- https://ww3.yggtorrent.is/
|
||||
legacylinks:
|
||||
- https://yggtorrent.is/
|
||||
- https://yggtorrent.com/
|
||||
- https://ww1.yggtorrent.com/
|
||||
- https://ww1.yggtorrent.is/
|
||||
- https://ww2.yggtorrent.is/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
@@ -243,6 +246,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:
|
||||
|
||||
@@ -1,102 +1,80 @@
|
||||
---
|
||||
site: yourexotic
|
||||
name: Your Exotic Torrents
|
||||
name: ExoticaZ
|
||||
description: "a porn tracker"
|
||||
language: en-us
|
||||
type: private
|
||||
encoding: UTF-8
|
||||
links:
|
||||
- https://exoticaz.to/
|
||||
legacylinks:
|
||||
- https://torrents.yourexotic.com/
|
||||
|
||||
caps:
|
||||
categorymappings:
|
||||
- {id: 11, cat: XXX, desc: "DVDRip"}
|
||||
- {id: 2, cat: XXX, desc: "DVDRip Censored"}
|
||||
- {id: 1, cat: XXX, desc: "Video Clip"}
|
||||
- {id: 20, cat: XXX, desc: "Censored Clips"}
|
||||
- {id: 14, cat: XXX, desc: "Hentai"}
|
||||
- {id: 19, cat: XXX, desc: "Full DVD"}
|
||||
- {id: 16, cat: XXX, desc: "HD 720p"}
|
||||
- {id: 17, cat: XXX, desc: "HD 1080p"}
|
||||
- {id: 18, cat: XXX, desc: "HD Censored"}
|
||||
- {id: 15, cat: XXX, desc: "SVCD/VCD"}
|
||||
- {id: 13, cat: XXX, desc: "Softcore"}
|
||||
- {id: 3, cat: XXX, desc: "Pictures"}
|
||||
- {id: 21, cat: XXX, desc: "Mixed Videos"}
|
||||
- {id: 1, cat: XXX, desc: "DVDRip"}
|
||||
modes:
|
||||
search: [q]
|
||||
tv-search: [q]
|
||||
movie-search: [q]
|
||||
|
||||
login:
|
||||
path: /index.php?page=login
|
||||
method: post
|
||||
path: /login
|
||||
method: form
|
||||
inputs:
|
||||
uid: "{{ .Config.username }}"
|
||||
pwd: "{{ .Config.password }}"
|
||||
username: "{{ .Config.username }}"
|
||||
password: "{{ .Config.password }}"
|
||||
remember: "on"
|
||||
error:
|
||||
- selector: .lista>span
|
||||
- selector: div.invalid-feedback
|
||||
test:
|
||||
path: index.php
|
||||
selector: form[name="jump1"]
|
||||
selector: div.ratio-bar
|
||||
|
||||
search:
|
||||
path: index.php
|
||||
path: /torrents
|
||||
inputs:
|
||||
$raw: "&category={{range .Categories}}{{.}};{{end}}"
|
||||
page: "torrents"
|
||||
active: "0"
|
||||
search: "{{ .Query.Keywords }}"
|
||||
in: 1
|
||||
search: "{{ .Keywords }}"
|
||||
category: 0
|
||||
rows:
|
||||
selector: table.lista > tbody > tr:has(a[href^="index.php?page=torrent-details&id="])
|
||||
selector: div.table-responsive > table > tbody > tr
|
||||
fields:
|
||||
category:
|
||||
selector: td:nth-child(1) a
|
||||
attribute: href
|
||||
filters:
|
||||
- name: querystring
|
||||
args: category
|
||||
text: 1
|
||||
title:
|
||||
selector: a[href^="index.php?page=torrent-details&id="]
|
||||
selector: a.torrent-link
|
||||
attribute: title
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["View details: ", ""]
|
||||
details:
|
||||
selector: a[href^="index.php?page=torrent-details&id="]
|
||||
selector: a.torrent-link
|
||||
attribute: href
|
||||
banner:
|
||||
selector: span.screen-image
|
||||
attribute: data-screens
|
||||
filters:
|
||||
- name: split
|
||||
args: ["|", 0]
|
||||
size:
|
||||
selector: td:nth-child(7)
|
||||
selector: td:nth-child(5)
|
||||
seeders:
|
||||
selector: td:nth-child(9)
|
||||
leechers:
|
||||
selector: td:nth-child(10)
|
||||
grabs:
|
||||
selector: td:nth-child(11)
|
||||
filters:
|
||||
- name: replace
|
||||
args: ["---", "0"]
|
||||
date:
|
||||
selector: td:nth-child(6)
|
||||
leechers:
|
||||
selector: td:nth-child(7)
|
||||
grabs:
|
||||
selector: td:nth-child(8)
|
||||
date:
|
||||
selector: td:nth-child(3)
|
||||
filters:
|
||||
- name: dateparse
|
||||
args: "02/01/2006"
|
||||
- name: append
|
||||
args: " ago"
|
||||
download:
|
||||
selector: a[href^="download.php"]
|
||||
selector: a[href*="/download/"]
|
||||
attribute: href
|
||||
downloadvolumefactor:
|
||||
case:
|
||||
img[src$="freeleech.gif"]: "0"
|
||||
img[src$="silver.gif"]: "0.5"
|
||||
i[title="Free Download"]: "0"
|
||||
i[title="Half Download"]: "0.5"
|
||||
"*": "1"
|
||||
uploadvolumefactor:
|
||||
case:
|
||||
img[src$="2x.gif"]: "2"
|
||||
img[src$="3x.gif"]: "3"
|
||||
img[src$="4x.gif"]: "4"
|
||||
img[src$="5x.gif"]: "5"
|
||||
img[src$="6x.gif"]: "6"
|
||||
img[src$="7x.gif"]: "7"
|
||||
img[src$="8x.gif"]: "8"
|
||||
img[src$="9x.gif"]: "9"
|
||||
i.fa-gem: "2"
|
||||
"*": "1"
|
||||
|
||||
@@ -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"];
|
||||
@@ -283,5 +313,29 @@ namespace Jackett.Common.Indexers.Abstract
|
||||
release.UploadVolumeFactor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<byte[]> Download(Uri link)
|
||||
{
|
||||
var content = await base.Download(link);
|
||||
|
||||
// Check if we're out of FL tokens/torrent is to large
|
||||
// most gazelle trackers will simply return the torrent anyway but e.g. redacted will return an error
|
||||
var requestLink = link.ToString();
|
||||
if (content.Length >= 1
|
||||
&& content[0] != 'd' // simple test for torrent vs HTML content
|
||||
&& requestLink.Contains("usetoken=1"))
|
||||
{
|
||||
var html = Encoding.GetString(content);
|
||||
if (html.Contains("You do not have any freeleech tokens left.")
|
||||
|| html.Contains("This torrent is too large."))
|
||||
{
|
||||
// download again with usetoken=0
|
||||
var requestLinkNew = requestLink.Replace("usetoken=1", "usetoken=0");
|
||||
content = await base.Download(new Uri(requestLinkNew));
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace Jackett.Common.Indexers
|
||||
{
|
||||
{ "agents of shield", "Agents of S.H.I.E.L.D."}
|
||||
};
|
||||
|
||||
public override string[] LegacySiteLinks { get; protected set; } = new string[] {
|
||||
"https://bj-share.me/"
|
||||
};
|
||||
|
||||
private ConfigurationDataBasicLoginWithRSSAndDisplay ConfigData
|
||||
{
|
||||
@@ -38,7 +42,7 @@ namespace Jackett.Common.Indexers
|
||||
public BJShare(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps)
|
||||
: base("BJ-Share",
|
||||
description: "A brazilian tracker.",
|
||||
link: "https://bj-share.me/",
|
||||
link: "https://bj-share.info/",
|
||||
caps: TorznabUtil.CreateDefaultTorznabTVCaps(),
|
||||
configService: configService,
|
||||
client: wc,
|
||||
@@ -50,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)
|
||||
@@ -262,7 +266,6 @@ namespace Jackett.Common.Indexers
|
||||
string groupTitle = null;
|
||||
string groupYearStr = null;
|
||||
var categoryStr = "";
|
||||
DateTime? groupPublishDate = null;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -34,6 +34,9 @@ namespace Jackett.Common.Indexers
|
||||
public string Type { get; protected set; }
|
||||
public virtual string ID { get { return GetIndexerID(GetType()); } }
|
||||
|
||||
[JsonConverter(typeof(EncodingJsonConverter))]
|
||||
public Encoding Encoding { get; protected set; }
|
||||
|
||||
public virtual bool IsConfigured { get; protected set; }
|
||||
protected Logger logger;
|
||||
protected IIndexerConfigurationService configurationService;
|
||||
@@ -174,42 +177,49 @@ namespace Jackett.Common.Indexers
|
||||
//TODO: Remove this section once users have moved off DPAPI
|
||||
private bool MigratedFromDPAPI(JToken jsonConfig)
|
||||
{
|
||||
var currentAssembly = Assembly.GetExecutingAssembly();
|
||||
bool runningLegacyOwin = 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 == "Jackett.dll" || x.ManifestModule.Name == "JackettConsole.exe")
|
||||
.Count() == 2;
|
||||
|
||||
if (runningLegacyOwin)
|
||||
if (EnvironmentUtil.IsRunningLegacyOwin)
|
||||
{
|
||||
//Still running legacy Owin and using the DPAPI, we don't want to migrate
|
||||
logger.Debug(ID + " - Running Owin, no need to migrate from DPAPI");
|
||||
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 framewrok, no access to the DPAPI, so don't bother trying to migrate
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -223,22 +233,29 @@ namespace Jackett.Common.Indexers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Info("Password could not be unprotected using Microsoft.AspNetCore.DataProtection, trying legacy: " + ex.ToString());
|
||||
if (ex.Message != "The provided payload cannot be decrypted because it was not protected with this protection provider.")
|
||||
{
|
||||
logger.Info($"Password could not be unprotected using Microsoft.AspNetCore.DataProtection - {ID} : " + ex);
|
||||
}
|
||||
|
||||
logger.Info($"Attempting legacy Unprotect - {ID} : ");
|
||||
|
||||
try
|
||||
{
|
||||
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;
|
||||
|
||||
logger.Info($"Password successfully migrated for {ID}");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.Info("Password could not be unprotected using legacy DPAPI: " + exception.ToString());
|
||||
logger.Info($"Password could not be unprotected using legacy DPAPI - {ID} : " + exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,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;
|
||||
@@ -735,26 +752,22 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
protected ICollection<int> MapTrackerCatToNewznab(string input)
|
||||
{
|
||||
var cats = new List<int>();
|
||||
if (null != input)
|
||||
{
|
||||
var mapping = categoryMapping.Where(m => m.TrackerCategory != null && m.TrackerCategory.ToLowerInvariant() == input.ToLowerInvariant()).FirstOrDefault();
|
||||
if (mapping != null)
|
||||
{
|
||||
cats.Add(mapping.NewzNabCategory);
|
||||
}
|
||||
if (input == null)
|
||||
return new List<int>();
|
||||
|
||||
// 1:1 category mapping
|
||||
try
|
||||
{
|
||||
var trackerCategoryInt = int.Parse(input);
|
||||
cats.Add(trackerCategoryInt + 100000);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// input is not an integer, continue
|
||||
}
|
||||
var cats = categoryMapping.Where(m => m.TrackerCategory != null && m.TrackerCategory.ToLowerInvariant() == input.ToLowerInvariant()).Select(c => c.NewzNabCategory).ToList();
|
||||
|
||||
// 1:1 category mapping
|
||||
try
|
||||
{
|
||||
var trackerCategoryInt = int.Parse(input);
|
||||
cats.Add(trackerCategoryInt + 100000);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// input is not an integer, continue
|
||||
}
|
||||
|
||||
return cats;
|
||||
}
|
||||
|
||||
@@ -835,9 +848,6 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
public override TorznabCapabilities TorznabCaps { get; protected set; }
|
||||
|
||||
[JsonConverter(typeof(EncodingJsonConverter))]
|
||||
public Encoding Encoding { get; protected set; }
|
||||
|
||||
private List<CategoryMapping> categoryMapping = new List<CategoryMapping>();
|
||||
protected WebClient webclient;
|
||||
protected readonly string downloadUrlBase = "";
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Jackett.Common.Indexers
|
||||
{
|
||||
public class BitCityReloaded : BaseWebIndexer
|
||||
{
|
||||
private string LoginUrl { get { return SiteLink + "login.php"; } }
|
||||
private string LoginUrl { get { return SiteLink + "login/index.php"; } }
|
||||
private string BrowseUrl { get { return SiteLink + "uebersicht.php"; } }
|
||||
private TimeZoneInfo germanyTz = TimeZoneInfo.CreateCustomTimeZone("W. Europe Standard Time", new TimeSpan(1, 0, 0), "W. Europe Standard Time", "W. Europe Standard Time");
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Jackett.Common.Indexers
|
||||
{
|
||||
Encoding = Encoding.UTF8;
|
||||
Language = "en-us";
|
||||
Type = "private";
|
||||
Type = "public";
|
||||
|
||||
AddCategoryMapping(5, TorznabCatType.PC0day, "Applications");
|
||||
AddCategoryMapping(17, TorznabCatType.AudioAudiobook, "Audio Books");
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Parser.Html;
|
||||
using Jackett.Common.Models.IndexerConfig.Bespoke;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.IndexerConfig;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
@@ -22,10 +23,11 @@ namespace Jackett.Common.Indexers
|
||||
{ get { return SiteLink + "takelogin.php"; } }
|
||||
private string BrowseUrl
|
||||
{ get { return SiteLink + "browse.php"; } }
|
||||
private bool TorrentHTTPSMode => configData.TorrentHTTPSMode.Value;
|
||||
|
||||
private new ConfigurationDataBasicLogin configData
|
||||
private new ConfigurationDataEliteTracker configData
|
||||
{
|
||||
get { return (ConfigurationDataBasicLogin)base.configData; }
|
||||
get { return (ConfigurationDataEliteTracker)base.configData; }
|
||||
set { base.configData = value; }
|
||||
}
|
||||
|
||||
@@ -37,10 +39,10 @@ namespace Jackett.Common.Indexers
|
||||
logger: logger,
|
||||
p: protectionService,
|
||||
client: webClient,
|
||||
configData: new ConfigurationDataBasicLogin()
|
||||
configData: new ConfigurationDataEliteTracker()
|
||||
)
|
||||
{
|
||||
Encoding = Encoding.UTF8;
|
||||
Encoding = Encoding.UTF8;
|
||||
Language = "fr-fr";
|
||||
Type = "private";
|
||||
|
||||
@@ -206,6 +208,13 @@ Encoding = Encoding.UTF8;
|
||||
release.Peers = ParseUtil.CoerceInt(Leechers.TextContent) + release.Seeders;
|
||||
release.Grabs = ParseUtil.CoerceLong(Grabs.TextContent);
|
||||
|
||||
if (TorrentHTTPSMode)
|
||||
{
|
||||
var linkHttps = Row.QuerySelector("td:nth-child(4)").QuerySelector("a").GetAttribute("href");
|
||||
var idTorrent = ParseUtil.GetArgumentFromQueryString(linkHttps, "id");
|
||||
release.Link = new Uri($"{SiteLink}download.php?id={idTorrent}&type=ssl");
|
||||
}
|
||||
|
||||
if (added.QuerySelector("img[alt^=\"TORRENT GRATUIT\"]") != null)
|
||||
release.DownloadVolumeFactor = 0;
|
||||
else if (added.QuerySelector("img[alt^=\"TORRENT SILVER\"]") != null)
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jackett.Common.Models;
|
||||
using Jackett.Common.Models.IndexerConfig;
|
||||
@@ -30,6 +31,7 @@ namespace Jackett.Common.Indexers
|
||||
string Language { get; }
|
||||
string LastError { get; set; }
|
||||
string ID { get; }
|
||||
Encoding Encoding { get; }
|
||||
|
||||
TorznabCapabilities TorznabCaps { get; }
|
||||
|
||||
|
||||
@@ -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.org/");
|
||||
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);
|
||||
|
||||
@@ -117,12 +117,17 @@ namespace Jackett.Common.Indexers
|
||||
return IndexerConfigurationStatus.RequiresTesting;
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
protected async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query, String seasonep)
|
||||
{
|
||||
var releases = new List<ReleaseInfo>();
|
||||
var searchString = query.GetQueryString();
|
||||
var pairs = new List<KeyValuePair<string, string>>();
|
||||
|
||||
if (seasonep != null)
|
||||
{
|
||||
searchString = query.SanitizedSearchTerm;
|
||||
}
|
||||
|
||||
pairs.Add(new KeyValuePair<string, string>("nyit_sorozat_resz", "true"));
|
||||
pairs.Add(new KeyValuePair<string, string>("miben", "name"));
|
||||
pairs.Add(new KeyValuePair<string, string>("tipus", "kivalasztottak_kozott"));
|
||||
@@ -198,8 +203,45 @@ namespace Jackett.Common.Indexers
|
||||
string catlink = qRow.Find("a:has(img[class='categ_link'])").First().Attr("href");
|
||||
string cat = ParseUtil.GetArgumentFromQueryString(catlink, "tipus");
|
||||
release.Category = MapTrackerCatToNewznab(cat);
|
||||
if (seasonep == null)
|
||||
releases.Add(release);
|
||||
|
||||
else
|
||||
{
|
||||
if (query.MatchQueryStringAND(release.Title, null, seasonep))
|
||||
{
|
||||
/* For sonnar if the search querry was english the title must be english also so we need to change the Description and Title */
|
||||
var temp = release.Title;
|
||||
|
||||
// releasedata everithing after Name.S0Xe0X
|
||||
String releasedata =release.Title.Split(new[] { seasonep }, StringSplitOptions.None)[1].Trim();
|
||||
|
||||
/* if the release name not contains the language we add it because it is know from category */
|
||||
if (cat.Contains("hun") && !releasedata.Contains("hun"))
|
||||
releasedata += ".hun";
|
||||
|
||||
// release description contains [imdb: ****] but we only need the data before it for title
|
||||
String[] description = {release.Description, ""};
|
||||
if (release.Description.Contains("[imdb:"))
|
||||
{
|
||||
description = release.Description.Split('[');
|
||||
description[1] = "[" + description[1];
|
||||
}
|
||||
|
||||
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
|
||||
Match match = Regex.Match(releasedata, @"^E\d\d?");
|
||||
if (seasonep.Length==3 && match.Success)
|
||||
release.Title = (description[0].Trim() + "." + seasonep.Trim() + releasedata.Trim('.')).Replace(' ', '.');
|
||||
|
||||
// add back imdb points to the description [imdb: 8.7]
|
||||
release.Description = temp+" "+ description[1];
|
||||
release.Description = release.Description.Trim();
|
||||
releases.Add(release);
|
||||
}
|
||||
}
|
||||
|
||||
releases.Add(release);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -209,5 +251,16 @@ namespace Jackett.Common.Indexers
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
||||
{
|
||||
var results = await PerformQuery(query, null);
|
||||
if (results.Count()==0 && query.IsTVSearch) // if we search for a localized title ncore can't handle any extra S/E information, search without it and AND filter the results. See #1450
|
||||
{
|
||||
results = await PerformQuery(query,query.GetEpisodeSearchString());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +352,9 @@ namespace Jackett.Common.Indexers
|
||||
var grabsStr = qRow.Find("td:nth-child(8)").Text();
|
||||
release.Grabs = ParseUtil.GetLongFromString(grabsStr);
|
||||
|
||||
var filesStr = qRow.Find("td:nth-child(7) > a").Text();
|
||||
release.Files = ParseUtil.GetLongFromString(filesStr);
|
||||
|
||||
var category = qRow.Find(".br_type > a").Attr("href").Replace("browse.php?cat=", string.Empty);
|
||||
release.Category = MapTrackerCatToNewznab(category);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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 Synthesiz3r : GazelleTracker
|
||||
{
|
||||
public Synthesiz3r(IIndexerConfigurationService configService, WebClient webClient, Logger logger, IProtectionService protectionService)
|
||||
: base(name: "Synthesiz3r",
|
||||
desc: "Synthesiz3r (ST3) is a Private Torrent Tracker for ELECTRONIC MUSIC",
|
||||
link: "https://synthesiz3r.com/",
|
||||
configService: configService,
|
||||
logger: logger,
|
||||
protectionService: protectionService,
|
||||
webClient: webClient,
|
||||
supportsFreeleechTokens: true
|
||||
)
|
||||
{
|
||||
Language = "en-us";
|
||||
Type = "private";
|
||||
TorznabCaps.SupportedMusicSearchParamsList = new List<string>() { "q", "album", "artist", "label", "year" };
|
||||
|
||||
AddCategoryMapping(1, TorznabCatType.Audio, "Music");
|
||||
AddCategoryMapping(2, TorznabCatType.PC, "Applications");
|
||||
AddCategoryMapping(3, TorznabCatType.Books, "E-Books");
|
||||
AddCategoryMapping(4, TorznabCatType.AudioAudiobook, "Audiobooks");
|
||||
AddCategoryMapping(5, TorznabCatType.Movies, "E-Learning Videos");
|
||||
AddCategoryMapping(6, TorznabCatType.TV, "Comedy");
|
||||
AddCategoryMapping(7, TorznabCatType.Books, "Comics");
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ namespace Jackett.Common.Indexers
|
||||
AddCategoryMapping(47, TorznabCatType.Other, "Fonts");
|
||||
AddCategoryMapping(43, TorznabCatType.PCMac, "Mac");
|
||||
|
||||
AddCategoryMapping(96, TorznabCatType.MoviesUHD, "Movie/4K");
|
||||
AddCategoryMapping(25, TorznabCatType.MoviesSD, "Movies/480p");
|
||||
AddCategoryMapping(11, TorznabCatType.MoviesBluRay, "Movies/Bluray");
|
||||
AddCategoryMapping(5, TorznabCatType.MoviesBluRay, "Movies/Bluray-Full");
|
||||
@@ -79,7 +80,7 @@ namespace Jackett.Common.Indexers
|
||||
AddCategoryMapping(22, TorznabCatType.MoviesForeign, "Movies/Non-English");
|
||||
AddCategoryMapping(13, TorznabCatType.Movies, "Movies/Packs");
|
||||
AddCategoryMapping(44, TorznabCatType.MoviesSD, "Movies/SD/x264");
|
||||
AddCategoryMapping(48, TorznabCatType.MoviesUHD, "Movies/x265");
|
||||
AddCategoryMapping(48, TorznabCatType.Movies, "Movies/x265");
|
||||
AddCategoryMapping(1, TorznabCatType.MoviesSD, "Movies/XviD");
|
||||
|
||||
AddCategoryMapping(17, TorznabCatType.Audio, "Music/Audio");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CommandLine;
|
||||
using Jackett.Common.Utils;
|
||||
using System;
|
||||
|
||||
namespace Jackett.Common.Models.Config
|
||||
@@ -83,7 +84,16 @@ namespace Jackett.Common.Models.Config
|
||||
if (options.ListenPublic && options.ListenPrivate)
|
||||
{
|
||||
Console.WriteLine("You can only use listen private OR listen publicly.");
|
||||
Engine.Exit(1);
|
||||
|
||||
//TODO: Remove once off Owin
|
||||
if (EnvironmentUtil.IsRunningLegacyOwin)
|
||||
{
|
||||
Engine.Exit(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// SSL Fix
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -44,20 +44,23 @@ namespace Jackett.Common.Models.DTO
|
||||
stringQuery.ExpandCatsToSubCats();
|
||||
|
||||
// try to build an IMDB Query
|
||||
var imdbID = ParseUtil.GetFullImdbID(stringQuery.SanitizedSearchTerm);
|
||||
TorznabQuery imdbQuery = null;
|
||||
if (imdbID != null)
|
||||
if (stringQuery.SanitizedSearchTerm.StartsWith("tt") && stringQuery.SanitizedSearchTerm.Length <= 9)
|
||||
{
|
||||
imdbQuery = new TorznabQuery()
|
||||
var imdbID = ParseUtil.GetFullImdbID(stringQuery.SanitizedSearchTerm);
|
||||
TorznabQuery imdbQuery = null;
|
||||
if (imdbID != null)
|
||||
{
|
||||
ImdbID = imdbID,
|
||||
Categories = stringQuery.Categories,
|
||||
Season = stringQuery.Season,
|
||||
Episode = stringQuery.Episode,
|
||||
};
|
||||
imdbQuery.ExpandCatsToSubCats();
|
||||
imdbQuery = new TorznabQuery()
|
||||
{
|
||||
ImdbID = imdbID,
|
||||
Categories = stringQuery.Categories,
|
||||
Season = stringQuery.Season,
|
||||
Episode = stringQuery.Episode,
|
||||
};
|
||||
imdbQuery.ExpandCatsToSubCats();
|
||||
|
||||
return imdbQuery;
|
||||
return imdbQuery;
|
||||
}
|
||||
}
|
||||
|
||||
return stringQuery;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Jackett.Common.Models.IndexerConfig.Bespoke
|
||||
{
|
||||
class ConfigurationDataEliteTracker : ConfigurationDataBasicLogin
|
||||
{
|
||||
public BoolItem TorrentHTTPSMode { get; private set; }
|
||||
|
||||
public ConfigurationDataEliteTracker()
|
||||
: base()
|
||||
{
|
||||
TorrentHTTPSMode = new BoolItem { Name = "Use https for tracker URL (Experimental)", Value = false };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,98 +53,43 @@ 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;
|
||||
case "httpclient2":
|
||||
RegisterWebClient<HttpWebClient2>(builder);
|
||||
break;
|
||||
case "safecurl":
|
||||
RegisterWebClient<UnixSafeCurlWebClient>(builder);
|
||||
break;
|
||||
case "libcurl":
|
||||
RegisterWebClient<UnixLibCurlWebClient>(builder);
|
||||
break;
|
||||
case "automatic":
|
||||
default:
|
||||
if (System.Environment.OSVersion.Platform != PlatformID.Unix)
|
||||
{
|
||||
RegisterWebClient<HttpWebClient>(builder);
|
||||
break;
|
||||
}
|
||||
var usehttpclient = DetectMonoCompatabilityWithHttpClient();
|
||||
if (usehttpclient)
|
||||
RegisterWebClient<HttpWebClient>(builder);
|
||||
else
|
||||
RegisterWebClient<UnixLibCurlWebClient>(builder);
|
||||
RegisterWebClient<HttpWebClient>(builder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterWebClient<WebClientType>(ContainerBuilder builder)
|
||||
{
|
||||
Engine.WebClientType = typeof(WebClientType);
|
||||
//TODO: Remove once off Owin
|
||||
if (EnvironmentUtil.IsRunningLegacyOwin)
|
||||
{
|
||||
Engine.WebClientType = typeof(WebClientType);
|
||||
}
|
||||
builder.RegisterType<WebClientType>().As<WebClient>();
|
||||
}
|
||||
|
||||
private ServerConfig BuildServerConfig(IComponentContext ctx)
|
||||
{
|
||||
var configService = ctx.Resolve<IConfigurationService>();
|
||||
// Load config
|
||||
var config = configService.GetConfig<ServerConfig>();
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfig(_runtimeSettings);
|
||||
}
|
||||
else
|
||||
{
|
||||
//We don't load these out of the config files as it could get confusing to users who accidently save.
|
||||
//In future we could flatten the serverconfig, and use command line parameters to override any configuration.
|
||||
config.RuntimeSettings = _runtimeSettings;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.APIKey))
|
||||
{
|
||||
// Check for legacy key config
|
||||
var apiKeyFile = Path.Combine(configService.GetAppDataFolder(), "api_key.txt");
|
||||
if (File.Exists(apiKeyFile))
|
||||
{
|
||||
config.APIKey = File.ReadAllText(apiKeyFile);
|
||||
}
|
||||
|
||||
// Check for legacy settings
|
||||
|
||||
var path = Path.Combine(configService.GetAppDataFolder(), "config.json"); ;
|
||||
var jsonReply = new JObject();
|
||||
if (File.Exists(path))
|
||||
{
|
||||
jsonReply = JObject.Parse(File.ReadAllText(path));
|
||||
config.Port = (int)jsonReply["port"];
|
||||
config.AllowExternal = (bool)jsonReply["public"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.APIKey))
|
||||
config.APIKey = StringUtil.GenerateRandom(32);
|
||||
|
||||
configService.SaveConfig(config);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.InstanceId))
|
||||
{
|
||||
config.InstanceId = StringUtil.GenerateRandom(64);
|
||||
configService.SaveConfig(config);
|
||||
}
|
||||
config.ConfigChanged();
|
||||
return config;
|
||||
return configService.BuildServerConfig(_runtimeSettings);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static bool DetectMonoCompatabilityWithHttpClient()
|
||||
{
|
||||
bool usehttpclient = false;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
Type monotype = Type.GetType("Mono.Runtime");
|
||||
if (monotype != null)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Security.Principal;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace Jackett.Common.Services
|
||||
@@ -48,8 +49,6 @@ namespace Jackett.Common.Services
|
||||
dir.SetAccessControl(directorySecurity);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("App config/log directory: " + GetAppDataFolder());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -240,5 +239,56 @@ namespace Jackett.Common.Services
|
||||
{
|
||||
return EnvironmentUtil.JackettVersion;
|
||||
}
|
||||
|
||||
public ServerConfig BuildServerConfig(RuntimeSettings runtimeSettings)
|
||||
{
|
||||
// Load config
|
||||
var config = GetConfig<ServerConfig>();
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfig(runtimeSettings);
|
||||
}
|
||||
else
|
||||
{
|
||||
//We don't load these out of the config files as it could get confusing to users who accidently save.
|
||||
//In future we could flatten the serverconfig, and use command line parameters to override any configuration.
|
||||
config.RuntimeSettings = runtimeSettings;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.APIKey))
|
||||
{
|
||||
// Check for legacy key config
|
||||
var apiKeyFile = Path.Combine(GetAppDataFolder(), "api_key.txt");
|
||||
if (File.Exists(apiKeyFile))
|
||||
{
|
||||
config.APIKey = File.ReadAllText(apiKeyFile);
|
||||
}
|
||||
|
||||
// Check for legacy settings
|
||||
|
||||
var path = Path.Combine(GetAppDataFolder(), "config.json"); ;
|
||||
var jsonReply = new JObject();
|
||||
if (File.Exists(path))
|
||||
{
|
||||
jsonReply = JObject.Parse(File.ReadAllText(path));
|
||||
config.Port = (int)jsonReply["port"];
|
||||
config.AllowExternal = (bool)jsonReply["public"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.APIKey))
|
||||
config.APIKey = StringUtil.GenerateRandom(32);
|
||||
|
||||
SaveConfig(config);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.InstanceId))
|
||||
{
|
||||
config.InstanceId = StringUtil.GenerateRandom(64);
|
||||
SaveConfig(config);
|
||||
}
|
||||
|
||||
config.ConfigChanged();
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Jackett.Common.Models.Config;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jackett.Common.Services.Interfaces
|
||||
{
|
||||
@@ -15,5 +16,6 @@ namespace Jackett.Common.Services.Interfaces
|
||||
List<string> GetCardigannDefinitionsFolders();
|
||||
void CreateOrMigrateSettings();
|
||||
void PerformMigration();
|
||||
ServerConfig BuildServerConfig(RuntimeSettings runtimeSettings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using Newtonsoft.Json;
|
||||
namespace Jackett.Common.Services
|
||||
{
|
||||
|
||||
class SerializeService : ISerializeService
|
||||
public class SerializeService : ISerializeService
|
||||
{
|
||||
public string Serialise(object obj)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -12,8 +12,10 @@ using System.Threading.Tasks;
|
||||
using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Models.GitHub;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using Jackett.Common.Utils;
|
||||
using Jackett.Common.Utils.Clients;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
@@ -28,14 +30,16 @@ namespace Jackett.Common.Services
|
||||
IConfigurationService configService;
|
||||
ManualResetEvent locker = new ManualResetEvent(false);
|
||||
ITrayLockService lockService;
|
||||
private ServerConfig serverConfig;
|
||||
bool forceupdatecheck = false;
|
||||
|
||||
public UpdateService(Logger l, WebClient c, IConfigurationService cfg, ITrayLockService ls)
|
||||
public UpdateService(Logger l, WebClient c, IConfigurationService cfg, ITrayLockService ls, ServerConfig sc)
|
||||
{
|
||||
logger = l;
|
||||
client = c;
|
||||
configService = cfg;
|
||||
lockService = ls;
|
||||
serverConfig = sc;
|
||||
}
|
||||
|
||||
private string ExePath()
|
||||
@@ -74,13 +78,12 @@ namespace Jackett.Common.Services
|
||||
|
||||
private async Task CheckForUpdates()
|
||||
{
|
||||
var config = Engine.ServerConfig;
|
||||
if (config.RuntimeSettings.NoUpdates)
|
||||
if (serverConfig.RuntimeSettings.NoUpdates)
|
||||
{
|
||||
logger.Info($"Updates are disabled via --NoUpdates.");
|
||||
return;
|
||||
}
|
||||
if (config.UpdateDisabled && !forceupdatecheck)
|
||||
if (serverConfig.UpdateDisabled && !forceupdatecheck)
|
||||
{
|
||||
logger.Info($"Skipping update check as it is disabled.");
|
||||
return;
|
||||
@@ -95,6 +98,12 @@ namespace Jackett.Common.Services
|
||||
return;
|
||||
}
|
||||
|
||||
bool trayIsRunning = false;
|
||||
if (isWindows)
|
||||
{
|
||||
trayIsRunning = Process.GetProcessesByName("JackettTray").Length > 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -112,7 +121,7 @@ namespace Jackett.Common.Services
|
||||
|
||||
var releases = JsonConvert.DeserializeObject<List<Release>>(response.Content);
|
||||
|
||||
if (!config.UpdatePrerelease)
|
||||
if (!serverConfig.UpdatePrerelease)
|
||||
{
|
||||
releases = releases.Where(r => !r.Prerelease).ToList();
|
||||
}
|
||||
@@ -132,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, config.RuntimeSettings.NoRestart);
|
||||
StartUpdate(updaterPath, installDir, isWindows, serverConfig.RuntimeSettings.NoRestart, trayIsRunning);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -209,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)
|
||||
{
|
||||
@@ -259,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
|
||||
@@ -278,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}\"";
|
||||
}
|
||||
@@ -295,16 +315,39 @@ 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();
|
||||
Engine.Exit(0);
|
||||
|
||||
//TODO: Remove once off Owin
|
||||
if (EnvironmentUtil.IsRunningLegacyOwin)
|
||||
{
|
||||
Engine.Exit(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ using CloudFlareUtilities;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using NLog;
|
||||
using Jackett.Common.Helpers;
|
||||
|
||||
namespace Jackett.Common.Utils.Clients
|
||||
{
|
||||
@@ -257,7 +258,10 @@ namespace Jackett.Common.Utils.Clients
|
||||
// See issue #1200
|
||||
if (result.RedirectingTo != null && result.RedirectingTo.StartsWith("file://"))
|
||||
{
|
||||
var newRedirectingTo = result.RedirectingTo.Replace("file://", request.RequestUri.Scheme + "://" + request.RequestUri.Host);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using CloudFlareUtilities;
|
||||
using Jackett.Common.Models.Config;
|
||||
using Jackett.Common.Services.Interfaces;
|
||||
using NLog;
|
||||
using Jackett.Common.Helpers;
|
||||
|
||||
namespace Jackett.Common.Utils.Clients
|
||||
{
|
||||
@@ -277,7 +278,10 @@ namespace Jackett.Common.Utils.Clients
|
||||
// See issue #1200
|
||||
if (result.RedirectingTo != null && result.RedirectingTo.StartsWith("file://"))
|
||||
{
|
||||
var newRedirectingTo = result.RedirectingTo.Replace("file://", request.RequestUri.Scheme + "://" + request.RequestUri.Host);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Jackett.Common.Utils
|
||||
@@ -22,6 +24,23 @@ namespace Jackett.Common.Utils
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsRunningLegacyOwin
|
||||
{
|
||||
get
|
||||
{
|
||||
bool runningOwin;
|
||||
|
||||
try
|
||||
{
|
||||
runningOwin = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName.StartsWith("Jackett, ")).Any();
|
||||
}
|
||||
catch
|
||||
{
|
||||
runningOwin = true;
|
||||
}
|
||||
|
||||
return runningOwin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,4 +118,4 @@ namespace Jackett.Common.Utils
|
||||
return "tt" + ((int)imdbid).ToString("D7");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,14 @@ namespace Jackett.Console
|
||||
Engine.Logger.Info("Jackett Data will be stored in: " + runtimeSettings.CustomDataFolder);
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(runtimeSettings.ClientOverride))
|
||||
{
|
||||
if (runtimeSettings.ClientOverride != "httpclient" && runtimeSettings.ClientOverride != "httpclient2" && runtimeSettings.ClientOverride != "httpclientnetcore")
|
||||
{
|
||||
Engine.Logger.Error($"Client override ({runtimeSettings.ClientOverride}) has been deprecated, please remove it from your start arguments");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Use Proxy
|
||||
if (options.ProxyConnection != null)
|
||||
|
||||
99
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
99
src/Jackett.Server/Controllers/BlackholeController.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
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 Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
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 = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
547
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
547
src/Jackett.Server/Controllers/ResultsController.cs
Normal file
@@ -0,0 +1,547 @@
|
||||
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();
|
||||
CurrentQuery.Categories = request.Category;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
/* We should allow this (helpful in case of aggregate indexers)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user