Compare commits

...

142 Commits

Author SHA1 Message Date
flightlevel
4bd7befb50 Update to .NET Core 2.1.2 2018-07-13 22:18:11 +10:00
flightlevel
56074155e9 Reverse proxy support for Kestrel 2018-07-13 22:15:09 +10:00
flightlevel
86a5a9cd25 Bump build version 2018-07-12 21:29:44 +10:00
flightlevel
1792ed276e Remove Windows Owin from build 2018-07-12 21:16:38 +10:00
flightlevel
4d1e4b59c8 Update to .NET Core 2.1.1 2018-07-11 19:37:19 +10:00
kaso17
e1ff4b9e12 GkTorrent: update links 2018-07-10 18:44:30 +02:00
kaso17
86400e1b8a Racing4Everyone: fix definition 2018-07-10 18:41:08 +02:00
kaso17
b3d53d1c01 WorldOfP2P: fix login 2018-07-10 18:27:02 +02:00
kaso17
471d494b3b Blutopia: fix definition 2018-07-10 18:09:53 +02:00
flightlevel
6e7d983fc3 Add extra logging 2018-07-10 22:22:02 +10:00
flightlevel
fd1073d0b5 Package update 2018-07-09 21:05:15 +10:00
flightlevel
7815615112 BTXpress: Change music to Audio 2018-07-09 21:00:42 +10:00
flightlevel
a6d8e68ca9 Minor changes for OMDB update 2018-07-09 21:00:17 +10:00
natalia2493
9593ba2414 Update BTXpress (#3365)
* Add btxpress.yml

* Update README.md

* Delete btworld

BTWorld is now known as BTXpress

* Update BTXpress

There was a issue with the selectors which has been fixed

* Update BTXpress

td:nth-child selectors were giving errors.

* Update btxpress.yml
2018-07-09 20:41:56 +10:00
HDVinnie
8c95045a4a (Update) blutopia.yml (#3357)
- added new input field "description" which is used to be able to search into torrent descriptions and media info using keywords
- This doesn't fix #3355 . Im not sure what the issue is. Maybe @kaso17 can check it out. I believe you still have a account there. If not let me know.
2018-07-09 20:41:32 +10:00
Tanel Puhu
667317e513 let's format the Imdb (#3354) 2018-07-09 20:40:37 +10:00
Jay Otterbein
0026a4f26e Allow configuration of omdb url (#3353)
OMDB recently added a private.omdbapi.com domain for supporters and this option allows the user to customize it while also allowing that url to change more rapidly in the future
2018-07-09 20:37:24 +10:00
Ivan de la Beldad Fernandez
2fac90df9f feat(mejortorrent): improve movie search (#3352)
* feat(newpct): rename title to make them more standarized

* fix: add site link configurable

* feat: add tests if news are not available

Sometimes the web has no news, and even if the tv show searcher is
working it throws an error. This way it will check news, and, if not
available, will check some series to check aliveness.

* fix: remove apostrophes from search

closes #3315

* fix: initialize search term if it's null

* fix: add multiepisode parsing and minor improvements

Now a quality it's ensured
Now quality it's cleaned from brackets
Add dash as multiepisode separator

* refactor: move tv search to function and change site link

Sitelink is now a property class

* refactor: extract function to get series uris

* refactor: extract function to get releases from one uri

* feat: add fallback to shows starting with "the"

Whenever a shows doesn't return results and doesn't start with "the"
search one more time prepending "the".

The motivation of this is because of Sonarr sanitizes queries before
sending them to Jackett. This indexer needs the article but Sonarr
removes it before.

* fix(newpct): add missing guid based on link

That was causing missing files on applications consuming the newpct
indexer.

* feat(mejortorrent): add movie search

closes: #3310

* feat(mejortorrent): add parse and filter by year

* feat(mejortorrent): change movie search algorithm

Now movie search is based on longest word, then results are filtered
ignoring special characters.

* fix(mejortorrent): restore query so jackett log it correctly

* fix: add small year correction

Some movies are tagged with the year before or after, I think it's safe to
fix years with exactly the same name and only a year of difference.
2018-07-09 20:35:30 +10:00
flightlevel
424e7b773d Mono 5.8 in readme 2018-07-06 21:04:31 +10:00
ATAlgaba
d25506e595 frozenlayer: Parse seasons greater or equal than 10 (#3341)
* Parse seasons greater than 10

* Update frozenlayer.yml

change * to +
2018-07-06 11:36:02 +02:00
ATAlgaba
a83e90e9ce XbytesV2: replace file languages (#3340)
Replace Xbytes language tag to language name in English
2018-07-06 11:33:49 +02:00
natalia2493
ea93182d96 Add BTXpress (#3339)
* Add btxpress.yml

* Update README.md

* Delete btworld

BTWorld is now known as BTXpress
2018-07-06 11:31:19 +02:00
HDVinnie
d60ee24111 blutopia: remove imdb field (#3313)
This doesn't fix anything but the IMDB link is no longer available
2018-07-04 18:57:57 +02:00
Ivan de la Beldad Fernandez
40e5c999b3 mejortorrent: add parse and filter by year (#3336)
* mejortorrent: add parse and filter by year
2018-07-04 18:54:40 +02:00
kaso17
6175b805d2 update firefox cookie screenshot 2018-07-04 18:42:27 +02:00
kaso17
5b6ba8d75f add firefox cookie screenshot 2018-07-04 18:40:02 +02:00
kaso17
10b0f0b1ce add cookies-chrome screenshot 2018-07-04 18:11:42 +02:00
Ivan de la Beldad Fernandez
a81ef63075 mejortorrent: add movies search (#3334)
* feat(mejortorrent): add movie search

fixes #3310
2018-07-04 17:10:21 +02:00
kaso17
2dc5edbb55 Merge branch 'master' of https://github.com/Jackett/Jackett 2018-07-04 09:49:08 +02:00
kaso17
636be458a6 TehConnection.me: add indexer 2018-07-04 09:48:56 +02:00
kaso17
64283f137c Zooqle: add andmatch 2018-07-04 09:47:48 +02:00
kaso17
ec1c9a9461 Mega-Bliz: add andmatch 2018-07-04 09:47:33 +02:00
Ivan de la Beldad Fernandez
d9a806d236 newpct: add guid (#3329)
* fix(newpct): add missing guid based on link

That was causing missing files on applications consuming the newpct
indexer.
2018-07-03 16:17:16 +02:00
Ivan de la Beldad Fernandez
ebd294b602 feat(newpct): add fallback when no releases are found (#3328)
* refactor: move tv search to function and change site link

Sitelink is now a property class

* refactor: extract function to get series uris

* refactor: extract function to get releases from one uri

* feat: add fallback to shows starting with "the"

Whenever a shows doesn't return results and doesn't start with "the"
search one more time prepending "the".

The motivation of this is because of Sonarr sanitizes queries before
sending them to Jackett. This indexer needs the article but Sonarr
removes it before.
2018-07-03 14:29:06 +02:00
Ivan de la Beldad Fernandez
6081094b73 newpct: fix parser multi-episode and minor improvements (#3327)
* fix: add multiepisode parsing and minor improvements

Now a quality it's ensured
Now quality it's cleaned from brackets
Add dash as multiepisode separator

* fix: prevent an error if cannot guess serie name from title

* fix: now title is being extracted correctly
2018-07-03 11:01:08 +02:00
Ivan de la Beldad Fernandez
115dfd20e7 Mejortorrent: improvements (#3324)
* feat(newpct): rename title to make them more standarized

* fix: add site link configurable

* feat: add tests if news are not available

Sometimes the web has no news, and even if the tv show searcher is
working it throws an error. This way it will check news, and, if not
available, will check some series to check aliveness.

* fix: remove apostrophes from search

closes #3315

* fix: initialize search term if it's null
2018-07-02 19:10:31 +02:00
kaso17
038fe2866b Merge branch 'master' of https://github.com/Jackett/Jackett 2018-07-02 14:44:43 +02:00
kaso17
aec40373d3 New Real World: fix parser 2018-07-02 14:44:34 +02:00
flightlevel
c9b8d27139 Disable .NET Core builds for now 2018-07-02 22:37:48 +10:00
flightlevel
d0793ebcba Merge branch 'master' into dotnetcore 2018-07-02 21:11:13 +10:00
flightlevel
00e4e8109e Reserve all addresses for port 2018-07-02 21:09:42 +10:00
kaso17
bcc1dc1948 fix custom certificate validation handler (#3297)
* fix netcore custom certificate validator

* conditional HttpWebClientNetCore register

* deprecate IgnoreSslErrors option

* Use httpclient when running full framework
2018-07-02 13:05:24 +02:00
flightlevel
1e67996df0 Tidy up logging renderer 2018-07-02 21:03:03 +10:00
Ivan de la Beldad Fernandez
947bed0a46 feat(newpct): rename title to make them more standarized (#3321) 2018-07-02 12:56:53 +02:00
flightlevel
0dd9842e84 Update Tests to net461 2018-07-01 14:40:06 +10:00
flightlevel
42728e2694 Update installer so that it kills Jackett when uninstalling 2018-06-30 22:49:11 +10:00
Celedhrim
44d14dc19c downloadville: fix definition (#3308) 2018-06-29 17:34:21 +02:00
kaso17
2e95c491a1 Blutopia: fix for new layout 2018-06-29 17:33:21 +02:00
kaso17
acfc776462 Torrent9: update domain 2018-06-29 15:16:13 +02:00
kaso17
b6692a7dab LostFilm: fix date parsing 2018-06-29 15:16:04 +02:00
kaso17
34fc2a0d15 cpasbien: update domain 2018-06-29 14:49:26 +02:00
kaso17
07938dcef8 BJShare: enable tracker specific categories 2018-06-29 14:16:41 +02:00
kaso17
847c5dfdcb Frozen Layer: improve title parsing (#3301)
Thank you @IhToN
2018-06-29 14:01:37 +02:00
ATAlgaba
bd47603f0d xbytes2: fix downloadvolumefactor parsing
Update _downloadvolumefactor_ as now they are using .png files
2018-06-28 18:22:00 +02:00
kaso17
8b86146715 YGGtorrent: fix typo 2018-06-28 16:50:45 +02:00
kaso17
5343c9109b YGGtorrent: fix size and add grabs 2018-06-28 16:46:00 +02:00
kaso17
687acae90b Torrent.LT: replace dots 2018-06-28 16:30:57 +02:00
kaso17
871e540957 The New Retro: add support for PIN 2018-06-28 16:21:47 +02:00
Akuma737
b0ce0b5350 metaltracker: Use Artist instead of keyword (#3295)
use artist instead of keyword if artist is supplied to make it somewhat compatible with lidarr(by default supplies artist and album with empty keyword)
2018-06-27 16:31:36 +02:00
flightlevel
bc965b1a0c Merge branch 'master' into dotnetcore 2018-06-27 22:12:53 +10:00
flightlevel
89b60c4c15 Update README.md 2018-06-27 22:11:57 +10:00
flightlevel
47a84775c5 Launch tray icon when Windows service updates 2018-06-27 22:02:51 +10:00
kaso17
8de0b0cbad ProtectionService: fix empty password handling 2018-06-26 17:58:46 +02:00
kaso17
b95fbd76a7 Merge branch 'master' of https://github.com/Jackett/Jackett 2018-06-26 17:47:29 +02:00
kaso17
6b44cc9b74 fix migration logic 2018-06-26 17:47:19 +02:00
flightlevel
0612c70ca1 Mono 5.8 is the minimum required 2018-06-26 20:21:21 +10:00
flightlevel
060972475f Windows Tray/Serice update improvements 2018-06-26 19:44:12 +10:00
kaso17
28bbeec462 MyAnonamouse: improve re-login detection 2018-06-24 18:38:49 +02:00
flightlevel
099adadbdc Don't need to clean BuildOutput 2018-06-24 18:07:41 +10:00
flightlevel
43511c6ecb Merge branch 'master' into dotnetcore 2018-06-24 18:06:09 +10:00
flightlevel
aa3e9c6fde Updater: Add version element 2018-06-24 18:04:59 +10:00
flightlevel
e53cdcb909 Update build script for Kestrel builds 2018-06-24 12:13:33 +10:00
flightlevel
fd14ad3f93 Remove Jackett.Service.Windows from solution file 2018-06-24 12:06:28 +10:00
flightlevel
ca1af97e42 Remove Jackett.Service.Windows
No longer needed now that Jackett.Service covers both scenarios
2018-06-24 12:04:27 +10:00
flightlevel
4970219ea7 Remove call to PlatformAbstractions 2018-06-24 12:00:59 +10:00
flightlevel
54c5b66ecf Merge branch 'master' into dotnetcore 2018-06-24 11:43:02 +10:00
flightlevel
8ae09d453d Remove Microsoft.Extensions.PlatformAbstractions dependency
Was only needed for .NET Core 1
2018-06-24 11:38:57 +10:00
flightlevel
f2e7ec25c8 Update build in preparation for Kestrel builds 2018-06-24 11:32:22 +10:00
flightlevel
7f9fff4683 Jackett.Updater: Make compatible with both legacy & .NET Core 2018-06-24 11:31:08 +10:00
Jorman
13f2eea298 TNTVillage: Update for better regex expression (#3276)
This tracker always respect the format
SXXEYY so I cleaned up the code, I only updated the code adding a simple regex, just in case a rel with XXxYY came up!
2018-06-24 00:50:58 +02:00
kaso17
d6f5a1b242 Merge branch 'master' of https://github.com/Jackett/Jackett 2018-06-23 10:26:31 +02:00
kaso17
ab37f0c2c7 Fuzer: add invisible recaptcha detection 2018-06-23 10:26:02 +02:00
flightlevel
9710b37064 Merge branch 'master' into dotnetcore 2018-06-23 11:39:19 +10:00
flightlevel
54f7568111 InnoSetup: Make paths parameters 2018-06-23 11:38:32 +10:00
flightlevel
06758964c0 Jackett.Service: Make compatible with both legacy & .NET Core 2018-06-23 11:37:49 +10:00
flightlevel
b56552e0f4 Updater: Remove unused reference 2018-06-23 11:36:29 +10:00
kaso17
ef8653f7d2 AST4u: add support for alternative layout 2018-06-22 17:32:44 +02:00
Ivan de la Beldad Fernandez
d5a6987390 add mejortorrent indexer (#3268)
* feat: start mejortorrent indexer, create basic configuration

* feat: add basic tv show scraper for series search

* fix: add guid so more than one episode is returned

* feat: add date to episodes

* feat: add support for multi-episodes

* feat: add proper filtering

* fix: change filter when there is no season or no episode

* feat: implement tv show rss scraper with no links by the moment

* feat: finish tv-shows rss

* fix: prevent sdtv to be marked as hdtv

* docs: add mejortorrent indexer to readme

* add url encode

* fix encoding

Content seems to be UTF8

* add missing include

* fix my fixes

* fix encoding
2018-06-22 16:48:33 +02:00
kaso17
8fb92ca05c Cinemageddon: attempt to fix download 2018-06-22 16:16:26 +02:00
morpheus133
11c7015c17 NCore: fix title generation (#3270) 2018-06-22 16:12:22 +02:00
flightlevel
8bc5d813b7 Tray Icon fix log message 2018-06-22 22:49:17 +10:00
flightlevel
012f5f05cc Update Logging 2018-06-22 22:35:58 +10:00
flightlevel
cd65ec7a71 Merge branch 'master' into dotnetcore 2018-06-22 22:32:48 +10:00
flightlevel
45826df4fe Make Jackett Tray compatible with both legacy & .NET Core projects 2018-06-22 22:29:45 +10:00
flightlevel
0645bab613 .NET Core preparation: Access Windows service 2018-06-22 22:21:10 +10:00
Jorman
4e409dfb50 Update certificate (#3257) 2018-06-19 18:32:22 +10:00
Raffaele
52a39b7a71 Updated categories (#3254)
After the "reboot" of the tracker many categories have changed.
2018-06-19 18:31:51 +10:00
flightlevel
5278b9fb47 Log Asp.Net.Core web requests 2018-06-18 22:01:25 +10:00
flightlevel
1ecafe3667 ResultsController - get BlackHole from ServerConfig without engine 2018-06-18 21:54:39 +10:00
flightlevel
190a415907 Remove unused runtimeSettings parameter 2018-06-18 21:50:09 +10:00
flightlevel
ca609a9e62 Add Legacy routes rewrite 2018-06-18 21:48:45 +10:00
flightlevel
b72ade7b27 Add ListenPublic/Private overrides 2018-06-17 14:39:49 +10:00
flightlevel
e7098d01c5 CommandLineParser: Case insensitive 2018-06-17 14:12:52 +10:00
flightlevel
b04ee56612 Process settings before creating WebHost 2018-06-17 14:02:13 +10:00
flightlevel
72a18e9b73 Get startup configuration without using DI 2018-06-17 12:39:03 +10:00
flightlevel
6be64bbe36 Merge branch 'master' into dotnetcore 2018-06-17 11:49:51 +10:00
flightlevel
1a14e8dd4b improve BEncode error handling: Pull in changes from master
3468e7d404
2018-06-16 18:40:31 +10:00
flightlevel
a25eb7f951 Merge branch 'master' into dotnetcore 2018-06-16 18:36:54 +10:00
flightlevel
9437cd54d3 Setup logging before DI 2018-06-16 18:32:08 +10:00
flightlevel
be55c5e4a6 Restart WebHost after port change in UI 2018-06-16 18:06:14 +10:00
flightlevel
68351a480b Merge branch 'master' into dotnetcore 2018-06-14 19:29:02 +10:00
flightlevel
33e35f1bd3 Re-enable the updater 2018-06-14 19:21:31 +10:00
flightlevel
e82b54994e Update build for Mono 2018-06-14 19:18:17 +10:00
flightlevel
211e152863 Merge branch 'master' into dotnetcore 2018-06-11 17:30:57 +10:00
flightlevel
5818b914a3 Include Mono version check 2018-06-10 22:31:55 +10:00
flightlevel
0680d39d90 Torznab: Pull in improved error handling
7f163c3945
2018-06-10 21:56:45 +10:00
flightlevel
4ca6676be0 Blackhole: Pull in fix magnet links
f3290800d8
2018-06-10 21:54:12 +10:00
flightlevel
3d85e751b7 Rewrite rule: Handle no path after base path 2018-06-10 21:52:13 +10:00
flightlevel
965da06214 Merge branch 'master' into dotnetcore 2018-06-10 12:53:02 +10:00
flightlevel
9abce7a586 Update build script so that binaries get pushed 2018-06-10 12:34:37 +10:00
flightlevel
53162b4bd3 Add back Windows Service functionality 2018-06-10 12:33:16 +10:00
flightlevel
80d78a027b Copy ServiceConfigService over untouched 2018-06-10 12:29:07 +10:00
flightlevel
3843d68766 Merge branch 'master' into dotnetcore 2018-06-05 21:53:56 +10:00
flightlevel
23f55ef33a Continue moving configuration across 2018-06-03 21:11:18 +10:00
flightlevel
e4c729a588 Merge branch 'master' into dotnetcore 2018-06-02 17:45:27 +10:00
flightlevel
f09c5722be Update to .NET core 2.1 RTM and setup builds 2018-06-02 17:33:26 +10:00
flightlevel
e73ae99e38 Implement AspNetCore.DataProtection to replace DPAPI 2018-05-30 21:43:58 +10:00
flightlevel
10c5c99385 Merge branch 'master' into dotnetcore 2018-05-30 21:30:58 +10:00
flightlevel
0a70f91bf1 Target full framework (Mono) as well for transition period 2018-05-20 21:51:52 +10:00
flightlevel
88945be5d7 Start configuration 2018-05-20 21:21:08 +10:00
flightlevel
5df131140f Upgrade to .NET Core 2.1 RC1 (ARM support) 2018-05-12 13:16:56 +10:00
flightlevel
8002483e68 Add response compression
Reduces dashboard load size from 677kb to 153kb
2018-05-12 12:51:46 +10:00
flightlevel
ce84264490 Add Authorization 2018-05-12 12:44:47 +10:00
flightlevel
615794a4bf Minor controller updates as the web app expects no content 2018-05-05 17:10:36 +10:00
flightlevel
5eed9d7038 Add middleware: Exception handling and rewrite/redirect 2018-05-05 17:08:46 +10:00
flightlevel
f162902b36 Refactor controllers for ASP.NET Core (Authentication disabled for now) 2018-05-01 22:55:09 +10:00
flightlevel
a752683965 Copy controllers untouced from Jackett project 2018-05-01 22:03:16 +10:00
flightlevel
683bd6e2d4 Add DI and Initialisation 2018-05-01 22:00:02 +10:00
flightlevel
13426fe7ec Refactor services for ASP.NET Core 2018-05-01 21:41:34 +10:00
flightlevel
d87d0f87b1 Add packages 2018-05-01 21:26:42 +10:00
flightlevel
85ec169755 Copy Services (except Windows Service) untouched from Jackett project 2018-05-01 21:17:59 +10:00
flightlevel
9cc40144a9 Add new DotNET Core API project Jackett.Server 2018-05-01 21:13:20 +10:00
95 changed files with 14516 additions and 11892 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

View File

@@ -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

View File

@@ -17,7 +17,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
#### Supported Systems
* Windows using .NET 4.6.1 or above [Download here](https://www.microsoft.com/net/framework/versions/net461).
* Linux and macOS using Mono 5.8 or above. [Download here](http://www.mono-project.com/download/). Earlier versions of mono may work, but some trackers may fail to negotiate SSL correctly, and others may cause Jackett to crash when used.
* Linux and macOS using Mono 5.8 or above. [Download here](http://www.mono-project.com/download/).
### Supported Public Trackers
* 1337x
@@ -42,6 +42,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
* KickAssTorrent (thekat.se clone)
* LimeTorrents
* MagnetDL
* MejorTorrent <!-- maintained by ivandelabeldad -->
* NextTorrent
* Newpct (aka: tvsinpagar, descargas2020, torrentlocura, torrentrapid, etc)
* Nyaa.si
@@ -125,7 +126,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
* BroadcastTheNet
* BrokenStones
* BTNext
* BTWorld
* BTXpress
* Carpathians
* CCFBits
* CGPeers
@@ -243,6 +244,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/
* Superbits
* Tasmanit
* TBPlus
* TehConnection.me
* TenYardTracker
* The Empire
* The Geeks
@@ -336,7 +338,7 @@ Mono must be compiled with the Roslyn compiler (default), using MCS will cause "
## Installation on macOS
### Prerequisites
Install [Mono 4.6](http://www.mono-project.com/download/#download-mac) or better (using the latest pkg installer is recommended).
Install [Mono 5.8](http://www.mono-project.com/download/#download-mac) or better (using the latest pkg installer is recommended).
* Setup ssl support by running `curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync --user /dev/stdin`
### Install as service
@@ -402,6 +404,7 @@ All contributions are welcome just send a pull request. Jackett's framework all
## Building from source
### Windows
* Install the .NET Core [SDK](https://www.microsoft.com/net/download/windows)
* Open the Jackett solution in Visual Studio 2017 (version 15.7 or above)
* Right click on the Jackett solution and click 'Rebuild Solution' to restore nuget packages
* Select Jackett.Console as startup project

View File

@@ -1,4 +1,4 @@
version: 0.8.{build}
version: 0.9.{build}
skip_tags: true
image: Visual Studio 2017
configuration: Release

View File

@@ -6,7 +6,7 @@
//////////////////////////////////////////////////////////////////////
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
var configuration = Argument("configuration", "Debug");
//////////////////////////////////////////////////////////////////////
// PREPARATION
@@ -36,8 +36,8 @@ Task("Clean")
.IsDependentOn("Info")
.Does(() =>
{
CleanDirectories("./src/**/obj" + configuration);
CleanDirectories("./src/**/bin" + configuration);
CleanDirectories("./src/**/obj");
CleanDirectories("./src/**/bin");
CleanDirectories("./BuildOutput");
CleanDirectories("./" + artifactsDirName);
CleanDirectories("./" + testResultsDirName);
@@ -56,7 +56,11 @@ Task("Build")
.IsDependentOn("Restore-NuGet-Packages")
.Does(() =>
{
MSBuild("./src/Jackett.sln", settings => settings.SetConfiguration(configuration));
var buildSettings = new MSBuildSettings()
.SetConfiguration(configuration)
.UseToolVersion(MSBuildToolVersion.VS2017);
MSBuild("./src/Jackett.sln", buildSettings);
});
Task("Run-Unit-Tests")
@@ -84,9 +88,7 @@ Task("Copy-Files-Full-Framework")
var windowsOutput = windowsBuildFullFramework + "/Jackett";
CopyDirectory("./src/Jackett.Console/bin/" + configuration, windowsOutput);
CopyFiles("./src/Jackett.Service/bin/" + configuration + "/JackettService.*", windowsOutput);
CopyFiles("./src/Jackett.Tray/bin/" + configuration + "/JackettTray.*", windowsOutput);
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/JackettUpdater.*", windowsOutput);
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net452" + "/JackettUpdater.*", windowsOutput); //builds against multiple frameworks
CopyFiles("./Upstart.config", windowsOutput);
CopyFiles("./LICENSE", windowsOutput);
CopyFiles("./README.md", windowsOutput);
@@ -115,49 +117,118 @@ Task("Check-Packaging-Platform")
}
});
Task("Package-Windows-Installer-Full-Framework")
.IsDependentOn("Check-Packaging-Platform")
.Does(() =>
{
InnoSetup("./Installer.iss", new InnoSetupSettings {
OutputDirectory = workingDir + "/" + artifactsDirName
});
});
Task("Package-Files-Full-Framework-Windows")
.IsDependentOn("Check-Packaging-Platform")
.Does(() =>
{
Zip(windowsBuildFullFramework, $"./{artifactsDirName}/Jackett.Binaries.Windows.zip");
Information(@"Full Framework Windows Binaries Zipping Completed");
});
Task("Package-Files-Full-Framework-Mono")
.IsDependentOn("Check-Packaging-Platform")
.Does(() =>
{
var cygMonoBuildPath = RelativeWinPathToCygPath(monoBuildFullFramework);
var tarFileName = "Jackett.Binaries.Mono.tar";
var tarArguments = @"-cvf " + cygMonoBuildPath + "/" + tarFileName + " -C " + cygMonoBuildPath + " Jackett --mode ='755'";
var gzipArguments = @"-k " + cygMonoBuildPath + "/" + tarFileName;
RunCygwinCommand("Tar", tarArguments);
RunCygwinCommand("Gzip", gzipArguments);
MoveFile($"{monoBuildFullFramework}/{tarFileName}.gz", $"./{artifactsDirName}/{tarFileName}.gz");
Gzip(monoBuildFullFramework, $"./{artifactsDirName}", "Jackett", "Jackett.Binaries.Mono.tar.gz");
Information(@"Full Framework Mono Binaries Gzip Completed");
});
Task("Package-Full-Framework")
.IsDependentOn("Package-Windows-Installer-Full-Framework")
.IsDependentOn("Package-Files-Full-Framework-Windows")
.IsDependentOn("Package-Files-Full-Framework-Mono")
.Does(() =>
{
Information("Full Framwork Packaging Completed");
Information("Full Framework Packaging Completed");
});
Task("Kestrel-Full-Framework")
.IsDependentOn("Package-Full-Framework")
.Does(() =>
{
CleanDirectories("./src/**/obj");
CleanDirectories("./src/**/bin");
NuGetRestore("./src/Jackett.sln");
var buildSettings = new MSBuildSettings()
.SetConfiguration(configuration)
.UseToolVersion(MSBuildToolVersion.VS2017);
MSBuild("./src/Jackett.sln", buildSettings);
});
Task("Experimental-Kestrel-Windows-Full-Framework")
.IsDependentOn("Kestrel-Full-Framework")
.Does(() =>
{
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
string buildOutputPath = "./BuildOutput/Experimental/net461/win7-x86/Jackett";
DotNetCorePublish(serverProjectPath, "net461", "win7-x86");
CopyFiles("./src/Jackett.Service/bin/" + configuration + "/JackettService.*", buildOutputPath);
CopyFiles("./src/Jackett.Tray/bin/" + configuration + "/JackettTray.*", buildOutputPath);
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net461" + "/JackettUpdater.*", buildOutputPath); //builds against multiple frameworks
Zip("./BuildOutput/Experimental/net461/win7-x86", $"./{artifactsDirName}/Jackett.Binaries.Windows.zip");
//InnoSetup
string sourceFolder = MakeAbsolute(Directory(buildOutputPath)).ToString();
InnoSetupSettings settings = new InnoSetupSettings();
settings.OutputDirectory = workingDir + "/" + artifactsDirName;
settings.Defines = new Dictionary<string, string>
{
{ "MyFileForVersion", sourceFolder + "/Jackett.Common.dll" },
{ "MySourceFolder", sourceFolder },
{ "MyOutputFilename", "Jackett.Installer.Windows" },
};
InnoSetup("./Installer.iss", settings);
});
Task("Experimental-Kestrel-Mono-Full-Framework")
.IsDependentOn("Kestrel-Full-Framework")
.Does(() =>
{
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
string buildOutputPath = "./BuildOutput/Experimental/net461/linux-x64/Jackett";
DotNetCorePublish(serverProjectPath, "net461", "linux-x64");
CopyFiles("./src/Jackett.Updater/bin/" + configuration + "/net461" + "/JackettUpdater.*", buildOutputPath); //builds against multiple frameworks
//There is an issue with Mono 5.8 (fixed in Mono 5.12) where its expecting to use its own patched version of System.Net.Http.dll, instead of the version supplied in folder
//https://github.com/dotnet/corefx/issues/19914
//https://bugzilla.xamarin.com/show_bug.cgi?id=60315
//The workaround is to delete System.Net.Http.dll and patch the .exe.config file
DeleteFile(buildOutputPath + "/System.Net.Http.dll");
var configFile = File(buildOutputPath + "/JackettConsole.exe.config");
XmlPoke(configFile, "configuration/runtime/*[name()='assemblyBinding']/*[name()='dependentAssembly']/*[name()='assemblyIdentity'][@name='System.Net.Http']/../*[name()='bindingRedirect']/@newVersion", "4.0.0.0");
Gzip("./BuildOutput/Experimental/net461/linux-x64", $"./{artifactsDirName}", "Jackett", "Experimental.Jackett.Binaries.Mono.tar.gz");
});
Task("Experimental-DotNetCore")
.IsDependentOn("Kestrel-Full-Framework")
.Does(() =>
{
string serverProjectPath = "./src/Jackett.Server/Jackett.Server.csproj";
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "win-x86");
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "linux-x64");
DotNetCorePublish(serverProjectPath, "netcoreapp2.1", "osx-x64");
Zip("./BuildOutput/Experimental/netcoreapp2.1/win-x86", $"./{artifactsDirName}/Experimental.netcoreapp.win-x86.zip");
Zip("./BuildOutput/Experimental/netcoreapp2.1/osx-x64", $"./{artifactsDirName}/Experimental.netcoreapp.osx-x64.zip");
Gzip("./BuildOutput/Experimental/netcoreapp2.1/linux-x64", $"./{artifactsDirName}", "Jackett", "Experimental.netcoreapp.linux-x64.tar.gz");
});
Task("Experimental")
.IsDependentOn("Experimental-Kestrel-Windows-Full-Framework")
.IsDependentOn("Experimental-Kestrel-Mono-Full-Framework")
//.IsDependentOn("Experimental-DotNetCore")
.Does(() =>
{
Information("Experimental builds completed");
});
Task("Appveyor-Push-Artifacts")
.IsDependentOn("Package-Full-Framework")
.IsDependentOn("Experimental")
.Does(() =>
{
if (AppVeyor.IsRunningOnAppVeyor)
@@ -263,10 +334,35 @@ private void RunCygwinCommand(string utility, string utilityArguments)
private string RelativeWinPathToCygPath(string relativePath)
{
var cygdriveBase = "/cygdrive/" + workingDir.ToString().Replace(":", "").Replace("\\", "/");
var cygPath = cygdriveBase + relativePath.Replace(".", "");
var cygPath = cygdriveBase + relativePath.TrimStart('.');
return cygPath;
}
private void Gzip(string sourceFolder, string outputDirectory, string tarCdirectoryOption, string outputFileName)
{
var cygSourcePath = RelativeWinPathToCygPath(sourceFolder);
var tarFileName = outputFileName.Remove(outputFileName.Length - 3, 3);
var tarArguments = @"-cvf " + cygSourcePath + "/" + tarFileName + " -C " + cygSourcePath + $" {tarCdirectoryOption} --mode ='755'";
var gzipArguments = @"-k " + cygSourcePath + "/" + tarFileName;
RunCygwinCommand("Tar", tarArguments);
RunCygwinCommand("Gzip", gzipArguments);
MoveFile($"{sourceFolder}/{tarFileName}.gz", $"{outputDirectory}/{tarFileName}.gz");
}
private void DotNetCorePublish(string projectPath, string framework, string runtime)
{
var settings = new DotNetCorePublishSettings
{
Framework = framework,
Runtime = runtime,
OutputDirectory = $"./BuildOutput/Experimental/{framework}/{runtime}/Jackett"
};
DotNetCorePublish(projectPath, settings);
}
//////////////////////////////////////////////////////////////////////
// TASK TARGETS
//////////////////////////////////////////////////////////////////////

View File

@@ -87,6 +87,7 @@ function loadJackettSettings() {
$("#jackett-prerelease").attr('checked', data.prerelease);
$("#jackett-logging").attr('checked', data.logging);
$("#jackett-omdbkey").val(data.omdbkey);
$("#jackett-omdburl").val(data.omdburl);
var password = data.password;
$("#jackett-adminpwd").val(password);
if (password != null && password != '') {
@@ -1150,6 +1151,7 @@ function bindUIButtons() {
var jackett_prerelease = $("#jackett-prerelease").is(':checked');
var jackett_logging = $("#jackett-logging").is(':checked');
var jackett_omdb_key = $("#jackett-omdbkey").val();
var jackett_omdb_url = $("#jackett-omdburl").val();
var jackett_proxy_url = $("#jackett-proxy-url").val();
var jackett_proxy_type = $("#jackett-proxy-type").val();
@@ -1166,6 +1168,7 @@ function bindUIButtons() {
logging: jackett_logging,
basepathoverride: jackett_basepathoverride,
omdbkey: jackett_omdb_key,
omdburl: jackett_omdb_url,
proxy_type: jackett_proxy_type,
proxy_url: jackett_proxy_url,
proxy_port: jackett_proxy_port,

View File

@@ -177,6 +177,10 @@
<span class="input-header">OMDB API key: </span>
<input id="jackett-omdbkey" class="form-control input-right" type="text" value="" placeholder="">
</div>
<div class="input-area">
<span class="input-header">OMDB API Url: </span>
<input id="jackett-omdburl" class="form-control input-right" type="text" value="" placeholder="Blank for default">
</div>
<hr />
<div id="footer">
<a href="https://github.com/Jackett/Jackett" target="_blank" title="Jackett on GitHub">Jackett</a> Version <span id="app-version"></span>
@@ -659,6 +663,6 @@
</script>
<script type="text/javascript" src="../libs/api.js?changed=2017083001"></script>
<script type="text/javascript" src="../custom.js?changed=2017110603"></script>
<script type="text/javascript" src="../custom.js?changed=20180709"></script>
</body>
</html>

View File

@@ -40,6 +40,18 @@
search: [q]
tv-search: [q, season, ep]
movie-search: [q]
settings:
- name: username
type: text
label: Username
- name: password
type: password
label: Password
- name: info
type: info
label: Results Per Page
default: For best results, change the 'Torrentliste' setting to "Platzsparendes Layout mit PopUp für zusätzliche Informationen" in your profile.
login:
path: takelogin.php
@@ -63,9 +75,13 @@
rows:
selector: table.tableinborder > tbody > tr:has(a[href^="details.php"])
fields:
fields: # note: two alternative layouts available
title:
selector: a[href^="details.php"]
title:
optional: true
selector: a[href^="details.php"][title]
attribute: title
category:
selector: a[href^="browse.php?cat="]
attribute: href
@@ -79,29 +95,31 @@
selector: a[href^=" /gettorrent/ssl/"]
attribute: href
files:
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(2)
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(2), a[href*="&filelist=1"]
grabs:
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(3) > b:nth-child(1)
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(3) > b:nth-child(1), a[href*="&tosnatchers=1"]
size:
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(1)
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(1) > b:nth-child(1), td:nth-child(7):has(br)
filters:
- name: replace
args: [".", ""]
- name: replace
args: [",", "."]
seeders:
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(1)
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(1), a[href*="&toseeders=1"]
leechers:
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(3)
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(2) > b:nth-child(3), a[href*="&todlers=1"]
date:
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(5)
selector: td:nth-child(2) > table > tbody > tr:nth-child(2) > td:nth-child(5), td:nth-child(5):has(br)
filters:
- name: replace
args: [" ", ""]
- name: append
args: " +2:00"
- name: replace
args: ["\xA0", " "]
args: ["\xA0", ""]
- name: dateparse
args: "02.01.2006 15:04:05 -07:00"
args: "02.01.200615:04:05 -07:00"
downloadvolumefactor:
case:
img[src="/pic/free.gif"]: "0"

View File

@@ -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"

View File

@@ -1,23 +1,24 @@
---
site: btworld
name: BTWorld
description: "HD Movie/TV Tracker"
site: btxpress
name: BTXpress
description: "HD Tracker Movies/TV/Music"
language: en-us
type: private
encoding: UTF-8
links:
- https://btworld.org/
- https://btxpress.org/
caps:
categorymappings:
- {id: 1, cat: Movies, desc: "Movies"}
- {id: 2, cat: TV, desc: "TV"}
- {id: 3, cat: Audio, desc: "Music"}
modes:
search: [q]
tv-search: [q, season, ep, imdbid]
movie-search: [q, imdbid]
login:
path: /login
method: form
@@ -28,13 +29,14 @@
- selector: table.main:contains("Login Failed!")
test:
path: /torrents
search:
paths:
- path: /filterTorrents
inputs:
$raw: "{{range .Categories}}categories[]={{.}}&{{end}}"
search: "{{if .Query.IMDBID}}{{else}}{{ .Keywords }}{{end}}"
description: ""
uploader: ""
imdb: "{{ .Query.IMDBIDShort }}"
tvdb: ""
@@ -70,10 +72,6 @@
details:
selector: a.view-torrent
attribute: href
imdb:
optional: true
selector: a[href*="://www.imdb.com/title/"]
attribute: href
size:
selector: td:nth-child(5)
seeders:

View File

@@ -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

View File

@@ -6,8 +6,9 @@
type: public
encoding: UTF-8
links:
- http://www.cpasbiens.cc/
- http://www.cpabien.io/
legacylinks:
- http://www.cpasbiens.cc/
- http://www.cpabien.cm/
- http://cpabien.cm/
- http://cpasbiens1.com/

View File

@@ -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"

View File

@@ -47,11 +47,33 @@
paths:
- path: "{{ if .Keywords }}buscar/descargas/{{ .Config.category }}/{{ .Keywords }}?search=Buscar{{else}}descargas{{end}}"
- path: "{{ if .Keywords }}buscar/descargas/{{ .Config.category }}/{{ .Keywords }}?search=Buscar&page=2{{else}}descargas{{end}}"
keywordsfilters:
- name: re_replace #remove S/EXX from search string
args: ["([SE]\\d{1,2})", ""]
rows:
selector: table#descargas > tbody > tr:has(td:has(a[href^="magnet:?"]))
fields:
title:
selector: td.tit a
filters:
- name: re_replace
args: ["\\/", " "]
- name: re_replace
args: ["\\(", ""]
- name: re_replace
args: ["\\)", ""]
- name: re_replace
args: ["([A-z]*) temporada", "S$1"]
- name: re_replace
args: ["S[pP]rimera", "S1"]
- name: re_replace
args: ["S[sS]egunda", "S2"]
- name: re_replace
args: ["S[tT]ercera", "S3"]
- name: re_replace
args: ["S([0-9]+) - Episodio ([0-9]+)", "S$1E$2"]
- name: re_replace
args: ["- Episodio ([0-9]*)", "E$1"]
details:
selector: td.tit a
attribute: href

View File

@@ -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/

View File

@@ -9,6 +9,7 @@
- https://ilcorsaronero.info/
certificates:
- aa7c40aa360a1cec8a9687312fd50402b912e618 # incomplete CA chain
- 83174ec1f92fa13cdef9d51888ea1dfba2166e17 # incomplete CA chain
caps:
categorymappings:

View File

@@ -82,6 +82,8 @@
rows:
selector: tr.browse_color
filters:
- name: andmatch
fields:
category:
selector: td:nth-of-type(1) a

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -31,33 +31,32 @@
- {id: 61, cat: Books, desc: "Audiolibri"}
# Games
- {id: 47, cat: PC/Games, desc: "Games PC"}
- {id: 22, cat: Console/Other, desc: "Nintendo"}
- {id: 40, cat: Console/Other, desc: "Nintendo"}
- {id: 13, cat: Console/PS4, desc: "Sony PS"}
- {id: 20, cat: Console/Xbox, desc: "XboX"}
- {id: 33, cat: Console/Xbox, desc: "XboX"}
- {id: 14, cat: Console/Wii, desc: "Wii"}
# Music
- {id: 54, cat: Audio/MP3, desc: "MP3"}
- {id: 55, cat: Audio/Lossless, desc: "Flac"}
# Movies
- {id: 17, cat: Movies/SD, desc: "Cine News"}
- {id: 23, cat: Movies/SD, desc: "BDRip"}
- {id: 43, cat: Movies/SD, desc: "BDRip"}
- {id: 16, cat: Movies/SD, desc: "DivX"}
- {id: 32, cat: Movies/SD, desc: "DVDRip"}
- {id: 11, cat: Movies/DVD, desc: "DVD"}
- {id: 29, cat: Movies/HD, desc: "720p"}
- {id: 30, cat: Movies/HD, desc: "1080p"}
- {id: 35, cat: Movies/BluRay, desc: "Blu Ray Disk"}
- {id: 40, cat: Movies/HD, desc: "H-265"}
- {id: 56, cat: Movies/3D, desc: "FullHD-3D"}
- {id: 27, cat: TV/SD, desc: "SerieTV"}
- {id: 20, cat: Movies/SD, desc: "DVDRip"}
- {id: 21, cat: Movies/DVD, desc: "DVD"}
- {id: 25, cat: Movies/HD, desc: "720p"}
- {id: 24, cat: Movies/HD, desc: "1080p"}
- {id: 27, cat: Movies/BluRay, desc: "Blu Ray Disk"}
- {id: 23, cat: Movies/HD, desc: "H-265"}
- {id: 26, cat: Movies/3D, desc: "3D-FullHD"}
- {id: 31, cat: TV/SD, desc: "SerieTV"}
- {id: 45, cat: TV/HD, desc: "Serie Tv HD"}
- {id: 44, cat: Movies/UHD, desc: "4K Ultra HD"}
- {id: 22, cat: Movies/UHD, desc: "4K-Ultra-HD"}
- {id: 49, cat: TV/Documentary, desc: "Documentari"}
- {id: 50, cat: TV/Other, desc: "Programmi TV"}
- {id: 51, cat: Movies/Other, desc: "Mp4"}
- {id: 5, cat: TV/Anime, desc: "Anime"}
- {id: 31, cat: TV/Anime, desc: "Cartoni Animati"}
modes:
search: [q]

View File

@@ -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

View File

@@ -6,8 +6,9 @@
type: public
encoding: UTF-8
links:
- http://www.torrent9.ec/
- http://www.torrent9.blue/
legacylinks:
- http://www.torrent9.ec/
- http://www.torrent9.red/
- http://www.torrent9.bz/
- http://www.torrents9.pe/

View File

@@ -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:

View File

@@ -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 }}"

View File

@@ -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:

View File

@@ -244,6 +244,11 @@
args: " ago"
size:
selector: "td:nth-child(6)"
filters:
- name: replace
args: ["o", "B"]
grabs:
selector: "td:nth-child(7)"
seeders:
text: 0
seeders:

View File

@@ -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

View File

@@ -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"));
}
}
}

View File

@@ -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"];

View File

@@ -54,28 +54,28 @@ namespace Jackett.Common.Indexers
Language = "pt-br";
Type = "private";
AddCategoryMapping(14, TorznabCatType.TVAnime); // Anime
AddCategoryMapping(3, TorznabCatType.PC0day); // Aplicativos
AddCategoryMapping(8, TorznabCatType.Other); // Apostilas/Tutoriais
AddCategoryMapping(19, TorznabCatType.AudioAudiobook); // Audiobook
AddCategoryMapping(16, TorznabCatType.TVOTHER); // Desenho Animado
AddCategoryMapping(18, TorznabCatType.TVDocumentary); // Documentários
AddCategoryMapping(10, TorznabCatType.Books); // E-Books
AddCategoryMapping(20, TorznabCatType.TVSport); // Esportes
AddCategoryMapping(1, TorznabCatType.Movies); // Filmes
AddCategoryMapping(12, TorznabCatType.MoviesOther); // Histórias em Quadrinhos
AddCategoryMapping(5, TorznabCatType.Audio); // Músicas
AddCategoryMapping(7, TorznabCatType.Other); // Outros
AddCategoryMapping(9, TorznabCatType.BooksMagazines); // Revistas
AddCategoryMapping(2, TorznabCatType.TV); // Seriados
AddCategoryMapping(17, TorznabCatType.TV); // Shows
AddCategoryMapping(13, TorznabCatType.TV); // Stand Up Comedy
AddCategoryMapping(11, TorznabCatType.Other); // Video-Aula
AddCategoryMapping(6, TorznabCatType.TV); // Vídeos de TV
AddCategoryMapping(4, TorznabCatType.Other); // Jogos
AddCategoryMapping(199, TorznabCatType.XXX); // Filmes Adultos
AddCategoryMapping(200, TorznabCatType.XXX); // Jogos Adultos
AddCategoryMapping(201, TorznabCatType.XXXImageset); // Fotos Adultas
AddCategoryMapping(14, TorznabCatType.TVAnime, "Anime");
AddCategoryMapping(3, TorznabCatType.PC0day, "Aplicativos");
AddCategoryMapping(8, TorznabCatType.Other, "Apostilas/Tutoriais");
AddCategoryMapping(19, TorznabCatType.AudioAudiobook, "Audiobook");
AddCategoryMapping(16, TorznabCatType.TVOTHER, "Desenho Animado");
AddCategoryMapping(18, TorznabCatType.TVDocumentary, "Documentários");
AddCategoryMapping(10, TorznabCatType.Books, "E-Books");
AddCategoryMapping(20, TorznabCatType.TVSport, "Esportes");
AddCategoryMapping(1, TorznabCatType.Movies, "Filmes");
AddCategoryMapping(12, TorznabCatType.MoviesOther, "Histórias em Quadrinhos");
AddCategoryMapping(5, TorznabCatType.Audio, "Músicas");
AddCategoryMapping(7, TorznabCatType.Other, "Outros");
AddCategoryMapping(9, TorznabCatType.BooksMagazines, "Revistas");
AddCategoryMapping(2, TorznabCatType.TV, "Seriados");
AddCategoryMapping(17, TorznabCatType.TV, "Shows");
AddCategoryMapping(13, TorznabCatType.TV, "Stand Up Comedy");
AddCategoryMapping(11, TorznabCatType.Other, "Video-Aula");
AddCategoryMapping(6, TorznabCatType.TV, "Vídeos de TV");
AddCategoryMapping(4, TorznabCatType.Other, "Jogos");
AddCategoryMapping(199, TorznabCatType.XXX, "Filmes Adultos");
AddCategoryMapping(200, TorznabCatType.XXX, "Jogos Adultos");
AddCategoryMapping(201, TorznabCatType.XXXImageset, "Fotos Adultas");
}
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)

View File

@@ -1,11 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using AutoMapper;
using AutoMapper;
using Jackett.Common.Models;
using Jackett.Common.Models.IndexerConfig;
using Jackett.Common.Services.Interfaces;
@@ -14,6 +7,13 @@ using Jackett.Common.Utils.Clients;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
namespace Jackett.Common.Indexers
{
@@ -184,10 +184,10 @@ namespace Jackett.Common.Indexers
return false;
}
Version dotNetVersion = Microsoft.Extensions.PlatformAbstractions.PlatformServices.Default.Application.RuntimeFramework.Version;
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("core", StringComparison.OrdinalIgnoreCase) >= 0;
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
if (!isWindows && dotNetVersion.Major < 4)
if (!isWindows && runningOnDotNetCore)
{
// User isn't running Windows, but is running on .NET Core framework, no access to the DPAPI, so don't bother trying to migrate
return false;
@@ -195,17 +195,31 @@ namespace Jackett.Common.Indexers
LoadValuesFromJson(jsonConfig, false);
object passwordPropertyValue = null;
StringItem passwordPropertyValue = null;
string passwordValue = "";
try
{
passwordPropertyValue = configData.GetType().GetProperty("Password").GetValue(configData, null);
passwordValue = passwordPropertyValue.GetType().GetProperty("Value").GetValue(passwordPropertyValue, null).ToString();
// try dynamic items first (e.g. all cardigann indexers)
passwordPropertyValue = (StringItem)configData.GetDynamicByName("password");
if (passwordPropertyValue == null) // if there's no dynamic password try the static property
{
passwordPropertyValue = (StringItem)configData.GetType().GetProperty("Password").GetValue(configData, null);
// protection is based on the item.Name value (property name might be different, example: Abnormal), so check the Name again
if (!string.Equals(passwordPropertyValue.Name, "password", StringComparison.InvariantCultureIgnoreCase))
{
logger.Debug($"Skipping non default password property (unencrpyted password) for [{ID}] while attempting migration");
return false;
}
}
passwordValue = passwordPropertyValue.Value;
}
catch (Exception)
{
logger.Debug($"Unable to source password for [{ID}] while attempting migration, likely a public tracker");
logger.Debug($"Unable to source password for [{ID}] while attempting migration, likely a tracker without a password setting");
return false;
}
@@ -231,7 +245,7 @@ namespace Jackett.Common.Indexers
string unprotectedPassword = protectionService.LegacyUnProtect(passwordValue);
//Password successfully unprotected using Windows/Mono DPAPI
passwordPropertyValue.GetType().GetProperty("Value").SetValue(passwordPropertyValue, unprotectedPassword);
passwordPropertyValue.Value = unprotectedPassword;
SaveConfig();
IsConfigured = true;
@@ -290,7 +304,7 @@ namespace Jackett.Common.Indexers
return false;
if (caps.SupportsImdbSearch && query.IsImdbQuery)
return true;
else if(!caps.SupportsImdbSearch && query.IsImdbQuery && query.QueryType != "TorrentPotato") // potato query should always contain imdb+search term
else if (!caps.SupportsImdbSearch && query.IsImdbQuery && query.QueryType != "TorrentPotato") // potato query should always contain imdb+search term
return false;
if (caps.SearchAvailable && query.IsSearch)
return true;
@@ -838,7 +852,6 @@ namespace Jackett.Common.Indexers
public override TorznabCapabilities TorznabCaps { get; protected set; }
private List<CategoryMapping> categoryMapping = new List<CategoryMapping>();
protected WebClient webclient;
protected readonly string downloadUrlBase = "";

View File

@@ -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> {

View File

@@ -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);

View File

@@ -0,0 +1,976 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html;
using Jackett.Common.Models;
using Jackett.Common.Models.IndexerConfig;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils.Clients;
using Jackett.Common.Helpers;
using Newtonsoft.Json.Linq;
using NLog;
namespace Jackett.Common.Indexers
{
class MejorTorrent : BaseWebIndexer
{
public static Uri WebUri = new Uri("http://www.mejortorrent.com/");
public static Uri DownloadUri = new Uri(WebUri, "secciones.php?sec=descargas&ap=contar_varios");
private static Uri SearchUriBase = new Uri(WebUri, "secciones.php");
public static Uri NewTorrentsUri = new Uri(WebUri, "secciones.php?sec=ultimos_torrents");
public static Encoding MEEncoding = Encoding.GetEncoding("windows-1252");
public MejorTorrent(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps)
: base(name: "MejorTorrent",
description: "MejorTorrent - Hay veces que un torrent viene mejor! :)",
link: WebUri.AbsoluteUri,
caps: new TorznabCapabilities(TorznabCatType.TV,
TorznabCatType.TVSD,
TorznabCatType.TVHD,
TorznabCatType.Movies),
configService: configService,
client: wc,
logger: l,
p: ps,
configData: new ConfigurationData())
{
Encoding = MEEncoding;
Language = "es-es";
Type = "public";
}
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
{
configData.LoadValuesFromJson(configJson);
WebUri = new Uri(configData.SiteLink.Value);
DownloadUri = new Uri(WebUri, "secciones.php?sec=descargas&ap=contar_varios");
SearchUriBase = new Uri(WebUri, "secciones.php");
NewTorrentsUri = new Uri(WebUri, "secciones.php?sec=ultimos_torrents");
var releases = await PerformQuery(new TorznabQuery());
await ConfigureIfOK(string.Empty, releases.Count() > 0, () =>
{
throw new Exception("Could not find releases from this URL");
});
return IndexerConfigurationStatus.Completed;
}
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
return await PerformQuery(query, 0);
}
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query, int attempts)
{
var originalSearchTerm = query.SearchTerm;
if (query.SearchTerm == null)
{
query.SearchTerm = "";
}
query.SearchTerm = query.SearchTerm.Replace("'", "");
var requester = new MejorTorrentRequester(this);
var tvShowScraper = new TvShowScraper();
var seasonScraper = new SeasonScraper();
var downloadScraper = new DownloadScraper();
var rssScraper = new RssScraper();
var downloadGenerator = new DownloadGenerator(requester, downloadScraper);
var tvShowPerformer = new TvShowPerformer(requester, tvShowScraper, seasonScraper, downloadGenerator);
var rssPerformer = new RssPerformer(requester, rssScraper, seasonScraper, downloadGenerator);
var movieSearchScraper = new MovieSearchScraper();
var movieInfoScraper = new MovieInfoScraper();
var movieDownloadScraper = new MovieDownloadScraper();
var moviePerformer = new MoviePerformer(requester, movieSearchScraper, movieInfoScraper, movieDownloadScraper);
var releases = new List<ReleaseInfo>();
if (string.IsNullOrEmpty(query.SanitizedSearchTerm))
{
releases = (await rssPerformer.PerformQuery(query)).ToList();
var movie = releases.First();
movie.Category.Add(TorznabCatType.Movies.ID);
releases.ToList().Add(movie);
if (releases.Count() == 0)
{
releases = (await AliveCheck(tvShowPerformer)).ToList();
}
return releases;
}
if (query.Categories.Contains(TorznabCatType.Movies.ID) || query.Categories.Count() == 0)
{
releases.AddRange(await moviePerformer.PerformQuery(query));
}
if (query.Categories.Contains(TorznabCatType.TV.ID) ||
query.Categories.Contains(TorznabCatType.TVSD.ID) ||
query.Categories.Contains(TorznabCatType.TVHD.ID) ||
query.Categories.Count() == 0)
{
releases.AddRange(await tvShowPerformer.PerformQuery(query));
}
query.SearchTerm = originalSearchTerm;
return releases;
}
private async Task<IEnumerable<ReleaseInfo>> AliveCheck(TvShowPerformer tvShowPerformer)
{
IEnumerable<ReleaseInfo> releases = new List<ReleaseInfo>();
var tests = new Queue<string>(new[] { "stranger things", "westworld", "friends" });
while (releases.Count() == 0 && tests.Count > 0)
{
var query = new TorznabQuery();
query.SearchTerm = tests.Dequeue();
releases = await tvShowPerformer.PerformQuery(query);
}
return releases;
}
public static Uri CreateSearchUri(string search)
{
var finalUri = SearchUriBase.AbsoluteUri;
finalUri += "?sec=buscador&valor=" + WebUtilityHelpers.UrlEncode(search, MEEncoding);
return new Uri(finalUri);
}
interface IScraper<T>
{
T Extract(IHtmlDocument html);
}
class RssScraper : IScraper<IEnumerable<KeyValuePair<MTReleaseInfo, Uri>>>
{
private readonly string LinkQuerySelector = "a[href*=\"/serie\"]";
public IEnumerable<KeyValuePair<MTReleaseInfo, Uri>> Extract(IHtmlDocument html)
{
var episodes = GetNewEpisodesScratch(html);
var links = GetLinks(html);
var results = new List<KeyValuePair<MTReleaseInfo, Uri>>();
for (var i = 0; i < episodes.Count(); i++)
{
results.Add(new KeyValuePair<MTReleaseInfo, Uri>(episodes.ElementAt(i), links.ElementAt(i)));
}
return results;
}
private List<MTReleaseInfo> GetNewEpisodesScratch(IHtmlDocument html)
{
var tvShowsElements = html.QuerySelectorAll(LinkQuerySelector);
var seasonLinks = tvShowsElements.Select(e => e.Attributes["href"].Value);
var dates = GetDates(html);
var titles = GetTitles(html);
var qualities = GetQualities(html);
var seasonsFirstEpisodesAndLast = GetSeasonsFirstEpisodesAndLast(html);
var episodes = new List<MTReleaseInfo>();
for(var i = 0; i < tvShowsElements.Count(); i++)
{
var e = new MTReleaseInfo();
e.TitleOriginal = titles.ElementAt(i);
e.PublishDate = dates.ElementAt(i);
e.CategoryText = qualities.ElementAt(i);
var sfeal = seasonsFirstEpisodesAndLast.ElementAt(i);
e.Season = sfeal.Key;
e.EpisodeNumber = sfeal.Value.Key;
if (sfeal.Value.Value != null && sfeal.Value.Value > sfeal.Value.Key)
{
e.Files = sfeal.Value.Value - sfeal.Value.Key + 1;
}
else
{
e.Files = 1;
}
episodes.Add(e);
}
return episodes;
}
private List<Uri> GetLinks(IHtmlDocument html)
{
return html.QuerySelectorAll(LinkQuerySelector)
.Select(e => e.Attributes["href"].Value)
.Select(relativeLink => new Uri(WebUri, relativeLink))
.ToList();
}
private List<DateTime> GetDates(IHtmlDocument html)
{
return html.QuerySelectorAll(LinkQuerySelector)
.Select(e => e.PreviousElementSibling.TextContent)
.Select(dateString => dateString.Split('-'))
.Select(parts => new int[] { Int32.Parse(parts[0]), Int32.Parse(parts[1]), Int32.Parse(parts[2]) })
.Select(intParts => new DateTime(intParts[0], intParts[1], intParts[2]))
.ToList();
}
private List<string> GetTitles(IHtmlDocument html)
{
var texts = LinkTexts(html);
var completeTitles = texts.Select(text => text.Substring(0, text.IndexOf('-') - 1));
var regex = new Regex(@".+\((.+)\)");
var finalTitles = completeTitles.Select(title =>
{
var match = regex.Match(title);
if (!match.Success) return title;
return match.Groups[1].Value;
});
return finalTitles.ToList();
}
private List<string> GetQualities(IHtmlDocument html)
{
var texts = LinkTexts(html);
var regex = new Regex(@".+\[(.*)\].+");
var qualities = texts.Select(text =>
{
var match = regex.Match(text);
if (!match.Success) return "HDTV";
var quality = match.Groups[1].Value;
switch(quality)
{
case "720p":
return "HDTV-720p";
case "1080p":
return "HDTV-1080p";
default:
return "HDTV";
}
});
return qualities.ToList();
}
private List<KeyValuePair<int, KeyValuePair<int,int?>>> GetSeasonsFirstEpisodesAndLast(IHtmlDocument html)
{
var texts = LinkTexts(html);
// SEASON | START EPISODE | [END EPISODE]
var regex = new Regex(@"(\d{1,2})x(\d{1,2})(?:.*\d{1,2}x(\d{1,2})?)?", RegexOptions.IgnoreCase);
var seasonsFirstEpisodesAndLast = texts.Select(text =>
{
var match = regex.Match(text);
int season = 0;
int episode = 0;
int? finalEpisode = null;
if (!match.Success) return new KeyValuePair<int, KeyValuePair<int, int?>>(season, new KeyValuePair<int, int?>(episode, finalEpisode));
season = Int32.Parse(match.Groups[1].Value);
episode = Int32.Parse(match.Groups[2].Value);
if (match.Groups[3].Success)
{
finalEpisode = Int32.Parse(match.Groups[3].Value);
}
return new KeyValuePair<int, KeyValuePair<int, int?>>(season, new KeyValuePair<int, int?>(episode, finalEpisode));
});
return seasonsFirstEpisodesAndLast.ToList();
}
private List<string> LinkTexts(IHtmlDocument html)
{
return html.QuerySelectorAll(LinkQuerySelector)
.Select(e => e.TextContent).ToList();
}
}
class TvShowScraper : IScraper<IEnumerable<Season>>
{
public IEnumerable<Season> Extract(IHtmlDocument html)
{
var tvSelector = "a[href*=\"/serie-\"]";
var seasonsElements = html.QuerySelectorAll(tvSelector).Select(e => e.ParentElement);
var newTvShows = new List<Season>();
// EXAMPLES:
// Stranger Things - 1ª Temporada (HDTV)
// Stranger Things - 1ª Temporada [720p] (HDTV-720p)
var regex = new Regex(@"(.+) - ([0-9]+).*\((.*)\)");
foreach (var seasonElement in seasonsElements)
{
var link = seasonElement.QuerySelector("a[href*=\"/serie-\"]").Attributes["href"].Value;
var info = seasonElement.TextContent; // Stranger Things - 1 ...
var searchMatch = regex.Match(info);
if (!searchMatch.Success)
{
continue;
}
int seasonNumber;
if (!Int32.TryParse(searchMatch.Groups[2].Value, out seasonNumber))
{
seasonNumber = 0;
}
var season = new Season
{
Title = searchMatch.Groups[1].Value,
Number = seasonNumber,
Type = searchMatch.Groups[3].Value,
Link = new Uri(WebUri, link)
};
// EXAMPLE: El cuento de la criada (Handmaids Tale)
var originalTitleRegex = new Regex(@".+\((.+)\)");
var originalTitleMath = originalTitleRegex.Match(season.Title);
if (originalTitleMath.Success)
{
season.Title = originalTitleMath.Groups[1].Value;
}
newTvShows.Add(season);
}
return newTvShows;
}
}
class SeasonScraper : IScraper<IEnumerable<MTReleaseInfo>>
{
public IEnumerable<MTReleaseInfo> Extract(IHtmlDocument html)
{
var episodesLinksHtml = html.QuerySelectorAll("a[href*=\"/serie-episodio-descargar-torrent\"]");
var episodesTexts = episodesLinksHtml.Select(l => l.TextContent).ToList();
var episodesLinks = episodesLinksHtml.Select(e => e.Attributes["href"].Value).ToList();
var dates = episodesLinksHtml
.Select(e => e.ParentElement.ParentElement.QuerySelector("div").TextContent)
.Select(stringDate => stringDate.Replace("Fecha: ", ""))
.Select(stringDate => stringDate.Split('-'))
.Select(stringParts => new int[]{ Int32.Parse(stringParts[0]), Int32.Parse(stringParts[1]), Int32.Parse(stringParts[2]) })
.Select(intParts => new DateTime(intParts[0], intParts[1], intParts[2]));
var episodes = episodesLinks.Select(e => new MTReleaseInfo()).ToList();
for (var i = 0; i < episodes.Count(); i++)
{
GuessEpisodes(episodes.ElementAt(i), episodesTexts.ElementAt(i));
ExtractLinkInfo(episodes.ElementAt(i), episodesLinks.ElementAt(i));
episodes.ElementAt(i).PublishDate = dates.ElementAt(i);
}
return episodes;
}
private void GuessEpisodes(MTReleaseInfo release, string episodeText)
{
var seasonEpisodeRegex = new Regex(@"(\d{1,2}).*?(\d{1,2})", RegexOptions.IgnoreCase);
var matchSeasonEpisode = seasonEpisodeRegex.Match(episodeText);
if (!matchSeasonEpisode.Success) return;
release.Season = Int32.Parse(matchSeasonEpisode.Groups[1].Value);
release.EpisodeNumber = Int32.Parse(matchSeasonEpisode.Groups[2].Value);
char[] textArray = episodeText.ToCharArray();
Array.Reverse(textArray);
var reversedText = new string(textArray);
var finalEpisodeRegex = new Regex(@"(\d{1,2})");
var matchFinalEpisode = finalEpisodeRegex.Match(reversedText);
if (!matchFinalEpisode.Success) return;
var finalEpisodeArray = matchFinalEpisode.Groups[1].Value.ToCharArray();
Array.Reverse(finalEpisodeArray);
var finalEpisode = Int32.Parse(new string(finalEpisodeArray));
if (finalEpisode > release.EpisodeNumber)
{
release.Files = (finalEpisode + 1) - release.EpisodeNumber;
release.Size = release.Size * release.Files;
}
}
private void ExtractLinkInfo(MTReleaseInfo release, String link)
{
// LINK FORMAT: /serie-episodio-descargar-torrent-${ID}-${TITLE}-${SEASON_NUMBER}x${EPISODE_NUMBER}[range].html
var regex = new Regex(@"\/serie-episodio-descargar-torrent-(\d+)-(.*)-(\d{1,2}).*(\d{1,2}).*\.html", RegexOptions.IgnoreCase);
var linkMatch = regex.Match(link);
if (!linkMatch.Success)
{
return;
}
release.MejorTorrentID = linkMatch.Groups[1].Value;
release.Title = linkMatch.Groups[2].Value;
}
}
class DownloadScraper : IScraper<IEnumerable<Uri>>
{
public IEnumerable<Uri> Extract(IHtmlDocument html)
{
return html.QuerySelectorAll("a[href*=\".torrent\"]")
.Select(e => e.Attributes["href"].Value)
.Select(link => new Uri(WebUri, link));
}
}
class Season
{
public String Title;
public int Number;
public Uri Link;
public TorznabCategory Category; // HDTV or HDTV-720
private string _type;
public string Type
{
get { return _type; }
set
{
switch(value)
{
case "HDTV":
Category = TorznabCatType.TVSD;
_type = "SDTV";
break;
case "HDTV-720p":
Category = TorznabCatType.TVHD;
_type = "HDTV-720p";
break;
case "HDTV-1080p":
Category = TorznabCatType.TVHD;
_type = "HDTV-1080p";
break;
default:
Category = TorznabCatType.TV;
_type = "HDTV-720p";
break;
}
}
}
}
class MTReleaseInfo : ReleaseInfo
{
public string MejorTorrentID;
public bool IsMovie;
public int? Year;
public int _season;
public int _episodeNumber;
private string _categoryText;
private string _originalTitle;
public MTReleaseInfo()
{
this.Category = new List<int>();
this.Grabs = 5;
this.Files = 1;
this.PublishDate = new DateTime();
this.Peers = 1;
this.Seeders = 1;
this.Size = ReleaseInfo.BytesFromGB(1);
this._originalTitle = "";
}
public int Season { get { return _season; } set { _season = value; TitleOriginal = _originalTitle; } }
public int EpisodeNumber { get { return _episodeNumber; } set { _episodeNumber = value; TitleOriginal = _originalTitle; } }
public string CategoryText {
get { return _categoryText; }
set
{
if (IsMovie)
{
Category.Add(TorznabCatType.Movies.ID);
_categoryText = value;
}
else
{
switch (value)
{
case "SDTV":
Category.Add(TorznabCatType.TVSD.ID);
_categoryText = "SDTV";
break;
case "HDTV":
Category.Add(TorznabCatType.TVSD.ID);
_categoryText = "SDTV";
break;
case "HDTV-720p":
Category.Add(TorznabCatType.TVHD.ID);
_categoryText = "HDTV-720p";
break;
case "HDTV-1080p":
Category.Add(TorznabCatType.TVHD.ID);
_categoryText = "HDTV-1080p";
break;
default:
Category.Add(TorznabCatType.TV.ID);
_categoryText = "HDTV-720p";
break;
}
}
TitleOriginal = _originalTitle;
}
}
public int FinalEpisodeNumber { get { return (int)(EpisodeNumber + Files - 1); } }
public string TitleOriginal
{
get { return _originalTitle; }
set
{
_originalTitle = value;
if (_originalTitle != "")
{
Title = _originalTitle;
Title = char.ToUpper(Title[0]) + Title.Substring(1);
}
var seasonAndEpisode = "";
if (!Category.Contains(TorznabCatType.Movies.ID))
{
seasonAndEpisode = "S" + Season.ToString("00") + "E" + EpisodeNumber.ToString("00");
if (Files > 1)
{
seasonAndEpisode += "-" + FinalEpisodeNumber.ToString("00");
}
}
Title = String.Join(".", new List<string>() { Title, seasonAndEpisode, CategoryText, "Spanish" }.Where(s => s != ""));
}
}
}
class MoviePerformer : IPerformer
{
private IRequester requester;
private IScraper<IEnumerable<Uri>> movieSearchScraper;
private IScraper<MTReleaseInfo> movieInfoScraper;
private IScraper<Uri> movieDownloadScraper;
public MoviePerformer(
IRequester requester,
IScraper<IEnumerable<Uri>> movieSearchScraper,
IScraper<MTReleaseInfo> movieInfoScraper,
IScraper<Uri> movieDownloadScraper)
{
this.requester = requester;
this.movieSearchScraper = movieSearchScraper;
this.movieInfoScraper = movieInfoScraper;
this.movieDownloadScraper = movieDownloadScraper;
}
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
query = SanitizeQuery(query);
var movies = await FetchMoviesBasedOnLongestWord(query);
return movies;
}
private async Task<IEnumerable<MTReleaseInfo>> FetchMoviesBasedOnLongestWord(TorznabQuery query)
{
var originalSearch = query.SearchTerm;
var regexStr = ".*" + originalSearch.Replace(" ", ".*") + ".*";
var regex = new Regex(regexStr, RegexOptions.IgnoreCase);
query.SearchTerm = LongestWord(query);
var movies = await FetchMovies(query);
return movies.Where(m => regex.Match(m.Title).Success);
}
private async Task<IEnumerable<MTReleaseInfo>> FetchMovies(TorznabQuery query)
{
var uriSearch = CreateSearchUri(query.SearchTerm);
var htmlSearch = await requester.MakeRequest(uriSearch);
var moviesInfoUris = movieSearchScraper.Extract(htmlSearch);
var infoHtmlTasks = moviesInfoUris.Select(async u => await requester.MakeRequest(u));
var infoHtmls = await Task.WhenAll(infoHtmlTasks);
var movies = infoHtmls.Select(h => movieInfoScraper.Extract(h));
var tasks = movies.Select(async m =>
{
var html = await requester.MakeRequest(m.Link);
return new KeyValuePair<MTReleaseInfo, IHtmlDocument>(m, html);
});
var moviesWithHtml = await Task.WhenAll(tasks.ToArray());
movies = moviesWithHtml.Select(movieWithHtml =>
{
var movie = movieWithHtml.Key;
var html = movieWithHtml.Value;
movie.Link = movieDownloadScraper.Extract(html);
movie.Guid = movieWithHtml.Key.Link;
return movie;
});
if (query.Year != null)
{
movies = movies
.Where(m =>
m.Year == query.Year ||
m.Year == query.Year + 1 ||
m.Year == query.Year - 1)
.Select(m =>
{
m.TitleOriginal = m.TitleOriginal.Replace("(" + m.Year + ")", "(" + query.Year + ")");
return m;
});
}
return movies;
}
private string LongestWord(TorznabQuery query)
{
var words = query.SearchTerm.Split(' ');
if (words.Count() == 0) return null;
var longestWord = words.First();
foreach(var word in words)
{
if (word.Length >= longestWord.Length)
{
longestWord = word;
}
}
return longestWord;
}
private TorznabQuery SanitizeQuery(TorznabQuery query)
{
var regex = new Regex(@"\d{4}$");
var match = regex.Match(query.SanitizedSearchTerm);
if (match.Success)
{
var yearStr = match.Groups[0].Value;
query.Year = Int32.Parse(yearStr);
query.SearchTerm = query.SearchTerm.Replace(yearStr, "").Trim();
}
return query;
}
}
class MovieSearchScraper : IScraper<IEnumerable<Uri>>
{
public IEnumerable<Uri> Extract(IHtmlDocument html)
{
return html.QuerySelectorAll("a[href*=\"/peli-\"]")
.Select(e => e.GetAttribute("href"))
.Select(relativeUri => new Uri(WebUri, relativeUri));
}
}
class MovieInfoScraper : IScraper<MTReleaseInfo>
{
public MTReleaseInfo Extract(IHtmlDocument html)
{
var release = new MTReleaseInfo();
release.IsMovie = true;
var selectors = html.QuerySelectorAll("b");
var titleSelector = html.QuerySelector("span>b");
try
{
var title = titleSelector.TextContent;
if (title.Contains("("))
{
title = title.Substring(0, title.IndexOf("(")).Trim();
}
release.TitleOriginal = title;
}
catch { }
try
{
var year = selectors.Where(s => s.TextContent.ToLower().Contains("año"))
.First().NextSibling.TextContent.Trim();
release.Year = Int32.Parse(year);
release.TitleOriginal += " (" + year + ")";
} catch { }
try
{
var dateStr = selectors.Where(s => s.TextContent.ToLower().Contains("fecha"))
.First().NextSibling.TextContent.Trim();
var date = Convert.ToDateTime(dateStr);
release.PublishDate = date;
} catch { }
try
{
var sizeStr = selectors.Where(s => s.TextContent.ToLower().Contains("tamaño"))
.First().NextSibling.TextContent.Trim();
Regex rgx = new Regex(@"[^0-9,.]");
long size;
if (sizeStr.ToLower().Trim().EndsWith("mb"))
{
size = ReleaseInfo.BytesFromMB(float.Parse(rgx.Replace(sizeStr, "")));
}
else
{
sizeStr = rgx.Replace(sizeStr, "").Replace(",", ".");
size = ReleaseInfo.BytesFromGB(float.Parse(rgx.Replace(sizeStr, "")));
}
release.Size = size;
} catch { }
try
{
var category = selectors.Where(s => s.TextContent.ToLower().Contains("formato"))
.First().NextSibling.TextContent.Trim();
release.CategoryText = category;
} catch { }
try
{
var title = titleSelector.TextContent;
if (title.Contains("(") && title.Contains(")") && title.Contains("4k"))
{
release.CategoryText = "2160p";
}
}
catch { }
try
{
var link = html.QuerySelector("a[href*=\"sec=descargas\"]").GetAttribute("href");
release.Link = new Uri(WebUri, link);
release.Guid = release.Link;
}
catch { }
return release;
}
}
class MovieDownloadScraper : IScraper<Uri>
{
public Uri Extract(IHtmlDocument html)
{
try
{
return new Uri(WebUri, html.QuerySelector("a[href*=\".torrent\"]").GetAttribute("href"));
}
catch
{
return null;
}
}
}
interface IRequester
{
Task<IHtmlDocument> MakeRequest(
Uri uri,
RequestType method = RequestType.GET,
IEnumerable<KeyValuePair<string, string>> data = null,
Dictionary<string, string> headers = null);
}
class MejorTorrentRequester : IRequester
{
private MejorTorrent mt;
public MejorTorrentRequester(MejorTorrent mt)
{
this.mt = mt;
}
public async Task<IHtmlDocument> MakeRequest(
Uri uri,
RequestType method = RequestType.GET,
IEnumerable<KeyValuePair<string, string>> data = null,
Dictionary<string, string> headers = null)
{
var result = await mt.RequestBytesWithCookies(uri.AbsoluteUri, null, method, null, data, headers);
var SearchResultParser = new HtmlParser();
var doc = SearchResultParser.Parse(mt.Encoding.GetString(result.Content));
return doc;
}
}
class MejorTorrentDownloadRequesterDecorator
{
private IRequester r;
public MejorTorrentDownloadRequesterDecorator(IRequester r)
{
this.r = r;
}
public async Task<IHtmlDocument> MakeRequest(IEnumerable<string> ids)
{
var downloadHtmlTasks = new List<Task<IHtmlDocument>>();
var formData = new List<KeyValuePair<string, string>>();
int index = 1;
ids.ToList().ForEach(id =>
{
var episodeID = new KeyValuePair<string, string>("episodios[" + index + "]", id);
formData.Add(episodeID);
index++;
});
formData.Add(new KeyValuePair<string, string>("total_capis", index.ToString()));
formData.Add(new KeyValuePair<string, string>("tabla", "series"));
return await r.MakeRequest(DownloadUri, RequestType.POST, formData);
}
}
interface IPerformer
{
Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query);
}
class RssPerformer : IPerformer
{
private IRequester requester;
private IScraper<IEnumerable<KeyValuePair<MTReleaseInfo, Uri>>> rssScraper;
private IScraper<IEnumerable<MTReleaseInfo>> seasonScraper;
private IDownloadGenerator downloadGenerator;
public RssPerformer(
IRequester requester,
IScraper<IEnumerable<KeyValuePair<MTReleaseInfo, Uri>>> rssScraper,
IScraper<IEnumerable<MTReleaseInfo>> seasonScraper,
IDownloadGenerator downloadGenerator)
{
this.requester = requester;
this.rssScraper = rssScraper;
this.seasonScraper = seasonScraper;
this.downloadGenerator = downloadGenerator;
}
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
var html = await requester.MakeRequest(NewTorrentsUri);
var episodesAndSeasonsUri = rssScraper.Extract(html);
Task.WaitAll(episodesAndSeasonsUri.ToList().Select(async epAndSeasonUri =>
{
var episode = epAndSeasonUri.Key;
var seasonUri = epAndSeasonUri.Value;
await AddMejorTorrentIDs(episode, seasonUri);
}).ToArray());
var episodes = episodesAndSeasonsUri.Select(epAndSeason => epAndSeason.Key).ToList();
await downloadGenerator.AddDownloadLinks(episodes);
return episodes;
}
private async Task AddMejorTorrentIDs(MTReleaseInfo episode, Uri seasonUri)
{
var html = await requester.MakeRequest(seasonUri);
var newEpisodes = seasonScraper.Extract(html);
// GET BY EPISODE NUMBER
newEpisodes = newEpisodes.Where(e => e.EpisodeNumber == episode.EpisodeNumber);
if (newEpisodes.Count() == 0)
{
throw new Exception("Imposible to detect episode ID in RSS");
}
episode.MejorTorrentID = newEpisodes.First().MejorTorrentID;
}
}
class TvShowPerformer : IPerformer
{
private IRequester requester;
private IScraper<IEnumerable<Season>> tvShowScraper;
private IScraper<IEnumerable<MTReleaseInfo>> seasonScraper;
private IDownloadGenerator downloadGenerator;
public TvShowPerformer(
IRequester requester,
IScraper<IEnumerable<Season>> tvShowScraper,
IScraper<IEnumerable<MTReleaseInfo>> seasonScraper,
IDownloadGenerator downloadGenerator)
{
this.requester = requester;
this.tvShowScraper = tvShowScraper;
this.seasonScraper = seasonScraper;
this.downloadGenerator = downloadGenerator;
}
public async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
{
query = FixQuery(query);
var seasons = await GetSeasons(query);
var episodes = await GetEpisodes(query, seasons);
await downloadGenerator.AddDownloadLinks(episodes);
if (seasons.Count() > 0)
{
episodes.ForEach(e => e.TitleOriginal = seasons.First().Title);
}
return episodes;
}
private TorznabQuery FixQuery(TorznabQuery query)
{
var seasonRegex = new Regex(@".*?(s\d{1,2})", RegexOptions.IgnoreCase);
var episodeRegex = new Regex(@".*?(e\d{1,2})", RegexOptions.IgnoreCase);
var seasonMatch = seasonRegex.Match(query.SearchTerm);
var episodeMatch = episodeRegex.Match(query.SearchTerm);
if (seasonMatch.Success)
{
query.Season = Int32.Parse(seasonMatch.Groups[1].Value.Substring(1));
query.SearchTerm = query.SearchTerm.Replace(seasonMatch.Groups[1].Value, "");
}
if (episodeMatch.Success)
{
query.Episode = episodeMatch.Groups[1].Value.Substring(1);
query.SearchTerm = query.SearchTerm.Replace(episodeMatch.Groups[1].Value, "");
}
query.SearchTerm = query.SearchTerm.Trim();
return query;
}
private async Task<List<Season>> GetSeasons(TorznabQuery query)
{
var seasonHtml = await requester.MakeRequest(CreateSearchUri(query.SanitizedSearchTerm));
var seasons = tvShowScraper.Extract(seasonHtml);
if (query.Season != 0)
{
seasons = seasons.Where(s => s.Number == query.Season);
}
if (query.Categories.Count() != 0)
{
seasons = seasons.Where(s => new List<int>(query.Categories).Contains(s.Category.ID));
}
return seasons.ToList();
}
private async Task<List<MTReleaseInfo>> GetEpisodes(TorznabQuery query, IEnumerable<Season> seasons)
{
var episodesHtmlTasks = new Dictionary<Season, Task<IHtmlDocument>>();
seasons.ToList().ForEach(season =>
{
episodesHtmlTasks.Add(season, requester.MakeRequest(new Uri(WebUri, season.Link)));
});
var episodesHtml = await Task.WhenAll(episodesHtmlTasks.Values);
var episodes = episodesHtmlTasks.SelectMany(seasonAndHtml =>
{
var season = seasonAndHtml.Key;
var html = seasonAndHtml.Value.Result;
var eps = seasonScraper.Extract(html);
return eps.ToList().Select(e =>
{
e.CategoryText = season.Type;
return e;
});
});
if (!string.IsNullOrEmpty(query.Episode))
{
var episodeNumber = Int32.Parse(query.Episode);
episodes = episodes.Where(e => e.EpisodeNumber <= episodeNumber && episodeNumber <= e.FinalEpisodeNumber);
}
return episodes.ToList();
}
}
interface IDownloadGenerator
{
Task AddDownloadLinks(IEnumerable<MTReleaseInfo> episodes);
}
class DownloadGenerator : IDownloadGenerator
{
private IRequester requester;
private IScraper<IEnumerable<Uri>> downloadScraper;
public DownloadGenerator(IRequester requester, IScraper<IEnumerable<Uri>> downloadScraper)
{
this.requester = requester;
this.downloadScraper = downloadScraper;
}
public async Task AddDownloadLinks(IEnumerable<MTReleaseInfo> episodes)
{
var downloadRequester = new MejorTorrentDownloadRequesterDecorator(requester);
var downloadHtml = await downloadRequester.MakeRequest(episodes.Select(e => e.MejorTorrentID));
var downloads = downloadScraper.Extract(downloadHtml).ToList();
for (var i = 0; i < downloads.Count; i++)
{
var e = episodes.ElementAt(i);
episodes.ElementAt(i).Link = downloads.ElementAt(i);
episodes.ElementAt(i).Guid = downloads.ElementAt(i);
}
}
}
}
}

View File

@@ -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);

View File

@@ -227,8 +227,7 @@ namespace Jackett.Common.Indexers
description = release.Description.Split('[');
description[1] = "[" + description[1];
}
else
release.Title = (description[0].Trim() + "." + seasonep.Trim() + "." + releasedata.Trim('.')).Replace(' ', '.');
// if search is done for S0X than we dont want to put . between S0X and E0X

View File

@@ -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();

View File

@@ -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);
}
}
}

View 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");
}
}
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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;

View File

@@ -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; }

View File

@@ -53,6 +53,9 @@ namespace Jackett.Common.Plumbing
// Register the best web client for the platform or the override
switch (_runtimeSettings.ClientOverride)
{
case "httpclientnetcore":
// do nothing, registered by the netcore app
break;
case "httpclient":
RegisterWebClient<HttpWebClient>(builder);
break;

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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()
{

View File

@@ -98,6 +98,12 @@ namespace Jackett.Common.Services
return;
}
bool trayIsRunning = false;
if (isWindows)
{
trayIsRunning = Process.GetProcessesByName("JackettTray").Length > 0;
}
try
{
@@ -135,7 +141,7 @@ namespace Jackett.Common.Services
var installDir = Path.GetDirectoryName(ExePath());
var updaterPath = Path.Combine(tempDir, "Jackett", "JackettUpdater.exe");
if (updaterPath != null)
StartUpdate(updaterPath, installDir, isWindows, serverConfig.RuntimeSettings.NoRestart);
StartUpdate(updaterPath, installDir, isWindows, serverConfig.RuntimeSettings.NoRestart, trayIsRunning);
}
catch (Exception e)
{
@@ -212,7 +218,7 @@ namespace Jackett.Common.Services
private async Task<string> DownloadRelease(List<Asset> assets, bool isWindows, string version)
{
var targetAsset = assets.Where(a => isWindows ? a.Browser_download_url.ToLowerInvariant().EndsWith(".zip") : a.Browser_download_url.ToLowerInvariant().EndsWith(".gz")).FirstOrDefault();
var targetAsset = assets.Where(a => isWindows ? a.Browser_download_url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) : a.Browser_download_url.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (targetAsset == null)
{
@@ -262,17 +268,29 @@ namespace Jackett.Common.Services
return tempDir;
}
private void StartUpdate(string updaterExePath, string installLocation, bool isWindows, bool NoRestart)
private void StartUpdate(string updaterExePath, string installLocation, bool isWindows, bool NoRestart, bool trayIsRunning)
{
string appType = "Console";
//DI once off Owin
IProcessService processService = new ProcessService(logger);
IServiceConfigService windowsService = new WindowsServiceConfigService(processService, logger);
if (isWindows && windowsService.ServiceExists() && windowsService.ServiceRunning())
{
appType = "WindowsService";
}
var exe = Path.GetFileName(ExePath());
var args = string.Join(" ", Environment.GetCommandLineArgs().Skip(1).Select(a => a.Contains(" ") ? "\"" +a + "\"" : a )).Replace("\"", "\\\"");
var startInfo = new ProcessStartInfo();
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
// Note: add a leading space to the --Args argument to avoid parsing as arguments
if (isWindows)
{
startInfo.Arguments = $"--Path \"{installLocation}\" --Type \"{exe}\" --Args \" {args}\"";
startInfo.Arguments = $"--Path \"{installLocation}\" --Type \"{appType}\" --Args \" {args}\"";
startInfo.FileName = Path.Combine(updaterExePath);
}
else
@@ -281,13 +299,12 @@ namespace Jackett.Common.Services
args = exe + " " + args;
exe = "mono";
startInfo.Arguments = $"{Path.Combine(updaterExePath)} --Path \"{installLocation}\" --Type \"{exe}\" --Args \" {args}\"";
startInfo.Arguments = $"{Path.Combine(updaterExePath)} --Path \"{installLocation}\" --Type \"{appType}\" --Args \" {args}\"";
startInfo.FileName = "mono";
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
}
try {
try
{
var pid = Process.GetCurrentProcess().Id;
startInfo.Arguments += $" --KillPids \"{pid}\"";
}
@@ -298,15 +315,30 @@ namespace Jackett.Common.Services
}
if (NoRestart)
{
startInfo.Arguments += " --NoRestart";
}
if (trayIsRunning && appType == "Console")
{
startInfo.Arguments += " --StartTray";
}
logger.Info($"Starting updater: {startInfo.FileName} {startInfo.Arguments}");
var procInfo = Process.Start(startInfo);
logger.Info($"Updater started process id: {procInfo.Id}");
if (NoRestart == false)
{
if (!NoRestart)
{
if (isWindows)
{
logger.Info("Signal sent to lock service");
lockService.Signal();
Thread.Sleep(2000);
}
logger.Info("Exiting Jackett..");
lockService.Signal();
//TODO: Remove once off Owin
if (EnvironmentUtil.IsRunningLegacyOwin)
{

View 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");
}
}
}
}

View File

@@ -32,17 +32,7 @@ namespace Jackett.Common.Utils
try
{
var currentAssembly = Assembly.GetExecutingAssembly();
bool aspNetCorePresent = new StackTrace().GetFrames()
.Select(x => x.GetMethod().ReflectedType.Assembly).Distinct()
.Where(x => x.GetReferencedAssemblies().Any(y => y.FullName == currentAssembly.FullName))
.Where(x => x.ManifestModule.Name == "JackettConsole.exe").Select(x => x.CustomAttributes)
.FirstOrDefault()
.Where(x => x.AttributeType.Assembly.FullName.StartsWith("Microsoft.AspNetCore", StringComparison.OrdinalIgnoreCase))
.Any();
runningOwin = !aspNetCorePresent;
runningOwin = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.FullName.StartsWith("Jackett, ")).Any();
}
catch
{

View 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"));
}
}
}
}

View File

@@ -0,0 +1,98 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using NLog;
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace Jackett.Server.Controllers
{
[AllowAnonymous]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
[Route("bh/{indexerID}")]
public class BlackholeController : Controller
{
private Logger logger;
private IIndexerManagerService indexerService;
private readonly ServerConfig serverConfig;
private IProtectionService protectionService;
public BlackholeController(IIndexerManagerService i, Logger l, ServerConfig config, IProtectionService ps)
{
logger = l;
indexerService = i;
serverConfig = config;
protectionService = ps;
}
[HttpGet]
public async Task<IActionResult> Blackhole(string indexerID, string path, string jackett_apikey, string file)
{
var jsonReply = new JObject();
try
{
var indexer = indexerService.GetWebIndexer(indexerID);
if (!indexer.IsConfigured)
{
logger.Warn(string.Format("Rejected a request to {0} which is unconfigured.", indexer.DisplayName));
throw new Exception("This indexer is not configured.");
}
if (serverConfig.APIKey != jackett_apikey)
throw new Exception("Incorrect API key");
path = WebUtility.UrlDecode(path);
path = protectionService.UnProtect(path);
var remoteFile = new Uri(path, UriKind.RelativeOrAbsolute);
var fileExtension = ".torrent";
var downloadBytes = await indexer.Download(remoteFile);
// handle magnet URLs
if (downloadBytes.Length >= 7
&& downloadBytes[0] == 0x6d // m
&& downloadBytes[1] == 0x61 // a
&& downloadBytes[2] == 0x67 // g
&& downloadBytes[3] == 0x6e // n
&& downloadBytes[4] == 0x65 // e
&& downloadBytes[5] == 0x74 // t
&& downloadBytes[6] == 0x3a // :
)
{
fileExtension = ".magnet";
}
if (string.IsNullOrWhiteSpace(serverConfig.BlackholeDir))
{
throw new Exception("Blackhole directory not set!");
}
if (!Directory.Exists(serverConfig.BlackholeDir))
{
throw new Exception("Blackhole directory does not exist: " + serverConfig.BlackholeDir);
}
var fileName = DateTime.Now.Ticks.ToString() + "-" + StringUtil.MakeValidFileName(indexer.DisplayName, '_', false);
if (string.IsNullOrWhiteSpace(file))
fileName += fileExtension;
else
fileName += "-" + StringUtil.MakeValidFileName(file + fileExtension, '_', false); // call MakeValidFileName() again to avoid any possibility of path traversal attacks
System.IO.File.WriteAllBytes(Path.Combine(serverConfig.BlackholeDir, fileName), downloadBytes);
jsonReply["result"] = "success";
}
catch (Exception ex)
{
logger.Error(ex, "Error downloading to blackhole " + indexerID + " " + path);
jsonReply["result"] = "error";
jsonReply["error"] = ex.Message;
}
return Json(jsonReply);
}
}
}

View 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();
}
}
}
}

View 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);
}
}
}
}

View File

@@ -0,0 +1,544 @@
using Jackett.Common;
using Jackett.Common.Indexers;
using Jackett.Common.Indexers.Meta;
using Jackett.Common.Models;
using Jackett.Common.Models.DTO;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using NLog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Jackett.Server.Controllers
{
public class RequiresApiKey : IActionFilter
{
public IServerService serverService;
public RequiresApiKey(IServerService ss)
{
serverService = ss;
}
public void OnActionExecuting(ActionExecutingContext context)
{
var validApiKey = serverService.GetApiKey();
var queryParams = context.HttpContext.Request.Query;
var queryApiKey = queryParams.Where(x => x.Key == "apikey" || x.Key == "passkey").Select(x => x.Value).FirstOrDefault();
#if DEBUG
if (Debugger.IsAttached)
{
return;
}
#endif
if (queryApiKey != validApiKey)
{
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.Unauthorized, 100, "Invalid API Key");
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public class RequiresConfiguredIndexer : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var controller = context.Controller;
if (!(controller is IIndexerController))
return;
var indexerController = controller as IIndexerController;
var parameters = context.RouteData.Values;
if (!parameters.ContainsKey("indexerId"))
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 200, "Indexer is not specified");
return;
}
var indexerId = parameters["indexerId"] as string;
if (indexerId.IsNullOrEmptyOrWhitespace())
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not specified (empty value)");
return;
}
var indexerService = indexerController.IndexerService;
var indexer = indexerService.GetIndexer(indexerId);
if (indexer == null)
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not supported");
return;
}
if (!indexer.IsConfigured)
{
indexerController.CurrentIndexer = null;
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.NotFound, 201, "Indexer is not configured");
return;
}
indexerController.CurrentIndexer = indexer;
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public class RequiresValidQuery : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
//TODO: Not sure what this is meant to do
//if (context.HttpContext.Response != null)
// return;
var controller = context.Controller;
if (!(controller is IResultController))
{
return;
}
var resultController = controller as IResultController;
var query = context.ActionArguments.First().Value;
var queryType = query.GetType();
var converter = queryType.GetMethod("ToTorznabQuery", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
if (converter == null)
{
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.BadRequest, 900, "ToTorznabQuery() not found");
}
var converted = converter.Invoke(null, new object[] { query });
var torznabQuery = converted as TorznabQuery;
resultController.CurrentQuery = torznabQuery;
if (queryType == typeof(ApiSearch)) // Skip CanHandleQuery() check for manual search (CurrentIndexer isn't used during manul search)
{
return;
}
if (!resultController.CurrentIndexer.CanHandleQuery(resultController.CurrentQuery))
{
context.Result = ResultsController.GetErrorActionResult(context.RouteData, HttpStatusCode.BadRequest, 201, $"{resultController.CurrentIndexer.ID} " +
$"does not support the requested query. Please check the capabilities (t=caps) and make sure the search mode and categories are supported.");
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public interface IResultController : IIndexerController
{
TorznabQuery CurrentQuery { get; set; }
}
[AllowAnonymous]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
[Route("api/v2.0/indexers/{indexerId}/results")]
[TypeFilter(typeof(RequiresApiKey))]
[TypeFilter(typeof(RequiresConfiguredIndexer))]
[TypeFilter(typeof(RequiresValidQuery))]
public class ResultsController : Controller, IResultController
{
public IIndexerManagerService IndexerService { get; private set; }
public IIndexer CurrentIndexer { get; set; }
public TorznabQuery CurrentQuery { get; set; }
private Logger logger;
private IServerService serverService;
private ICacheService cacheService;
private Common.Models.Config.ServerConfig serverConfig;
public ResultsController(IIndexerManagerService indexerManagerService, IServerService ss, ICacheService c, Logger logger, Common.Models.Config.ServerConfig sConfig)
{
IndexerService = indexerManagerService;
serverService = ss;
cacheService = c;
this.logger = logger;
serverConfig = sConfig;
}
[Route("")]
[HttpGet]
public async Task<IActionResult> Results([FromQuery] ApiSearch requestt)
{
//TODO: Better way to parse querystring
ApiSearch request = new ApiSearch();
foreach (var t in Request.Query)
{
if (t.Key == "Tracker[]")
{
request.Tracker = t.Value.ToString().Split(',');
}
if (t.Key == "Category[]")
{
request.Category = t.Value.ToString().Split(',').Select(Int32.Parse).ToArray();
}
if (t.Key == "query")
{
request.Query = t.Value.ToString();
}
}
var manualResult = new ManualSearchResult();
var trackers = IndexerService.GetAllIndexers().ToList().Where(t => t.IsConfigured);
if (request.Tracker != null)
{
trackers = trackers.Where(t => request.Tracker.Contains(t.ID));
}
trackers = trackers.Where(t => t.CanHandleQuery(CurrentQuery));
var tasks = trackers.ToList().Select(t => t.ResultsForQuery(CurrentQuery)).ToList();
try
{
var aggregateTask = Task.WhenAll(tasks);
await aggregateTask;
}
catch (AggregateException aex)
{
foreach (var ex in aex.InnerExceptions)
{
logger.Error(ex);
}
}
catch (Exception ex)
{
logger.Error(ex);
}
manualResult.Indexers = tasks.Select(t =>
{
var resultIndexer = new ManualSearchResultIndexer();
IIndexer indexer = null;
if (t.Status == TaskStatus.RanToCompletion)
{
resultIndexer.Status = ManualSearchResultIndexerStatus.OK;
resultIndexer.Results = t.Result.Releases.Count();
resultIndexer.Error = null;
indexer = t.Result.Indexer;
}
else if (t.Exception.InnerException is IndexerException)
{
resultIndexer.Status = ManualSearchResultIndexerStatus.Error;
resultIndexer.Results = 0;
resultIndexer.Error = ((IndexerException)t.Exception.InnerException).ToString();
indexer = ((IndexerException)t.Exception.InnerException).Indexer;
}
else
{
resultIndexer.Status = ManualSearchResultIndexerStatus.Unknown;
resultIndexer.Results = 0;
resultIndexer.Error = null;
}
if (indexer != null)
{
resultIndexer.ID = indexer.ID;
resultIndexer.Name = indexer.DisplayName;
}
return resultIndexer;
}).ToList();
manualResult.Results = tasks.Where(t => t.Status == TaskStatus.RanToCompletion).Where(t => t.Result.Releases.Any()).SelectMany(t =>
{
var searchResults = t.Result.Releases;
var indexer = t.Result.Indexer;
cacheService.CacheRssResults(indexer, searchResults);
return searchResults.Select(result =>
{
var item = AutoMapper.Mapper.Map<TrackerCacheResult>(result);
item.Tracker = indexer.DisplayName;
item.TrackerId = indexer.ID;
item.Peers = item.Peers - item.Seeders; // Use peers as leechers
return item;
});
}).OrderByDescending(d => d.PublishDate).ToList();
ConfigureCacheResults(manualResult.Results);
logger.Info(string.Format("Manual search for \"{0}\" on {1} with {2} results.", CurrentQuery.SanitizedSearchTerm, string.Join(", ", manualResult.Indexers.Select(i => i.ID)), manualResult.Results.Count()));
return Json(manualResult);
}
[Route("[action]/{ignored?}")]
[HttpGet]
public async Task<IActionResult> Torznab([FromQuery]TorznabRequest request)
{
if (string.Equals(CurrentQuery.QueryType, "caps", StringComparison.InvariantCultureIgnoreCase))
{
return Content(CurrentIndexer.TorznabCaps.ToXml(), "application/rss+xml", Encoding.UTF8);
}
// indexers - returns a list of all included indexers (meta indexers only)
if (string.Equals(CurrentQuery.QueryType, "indexers", StringComparison.InvariantCultureIgnoreCase))
{
if (!(CurrentIndexer is BaseMetaIndexer)) // shouldn't be needed because CanHandleQuery should return false
{
logger.Warn($"A search request with t=indexers from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} isn't a meta indexer.");
return GetErrorXML(203, "Function Not Available: this isn't a meta indexer");
}
var CurrentBaseMetaIndexer = (BaseMetaIndexer)CurrentIndexer;
var indexers = CurrentBaseMetaIndexer.Indexers;
if (string.Equals(request.configured, "true", StringComparison.InvariantCultureIgnoreCase))
indexers = indexers.Where(i => i.IsConfigured);
else if (string.Equals(request.configured, "false", StringComparison.InvariantCultureIgnoreCase))
indexers = indexers.Where(i => !i.IsConfigured);
var xdoc = new XDocument(
new XDeclaration("1.0", "UTF-8", null),
new XElement("indexers",
from i in indexers
select new XElement("indexer",
new XAttribute("id", i.ID),
new XAttribute("configured", i.IsConfigured),
new XElement("title", i.DisplayName),
new XElement("description", i.DisplayDescription),
new XElement("link", i.SiteLink),
new XElement("language", i.Language),
new XElement("type", i.Type),
i.TorznabCaps.GetXDocument().FirstNode
)
)
);
return Content(xdoc.Declaration.ToString() + Environment.NewLine + xdoc.ToString(), "application/xml", Encoding.UTF8);
}
if (CurrentQuery.ImdbID != null)
{
if (!string.IsNullOrEmpty(CurrentQuery.SearchTerm))
{
logger.Warn($"A search request from {Request.HttpContext.Connection.RemoteIpAddress} was made containing q and imdbid.");
return GetErrorXML(201, "Incorrect parameter: please specify either imdbid or q");
}
CurrentQuery.ImdbID = ParseUtil.GetFullImdbID(CurrentQuery.ImdbID); // normalize ImdbID
if (CurrentQuery.ImdbID == null)
{
logger.Warn($"A search request from {Request.HttpContext.Connection.RemoteIpAddress} was made with an invalid imdbid.");
return GetErrorXML(201, "Incorrect parameter: invalid imdbid format");
}
if (!CurrentIndexer.TorznabCaps.SupportsImdbSearch)
{
logger.Warn($"A search request with imdbid from {Request.HttpContext.Connection.RemoteIpAddress} was made but the indexer {CurrentIndexer.DisplayName} doesn't support it.");
return GetErrorXML(203, "Function Not Available: imdbid is not supported by this indexer");
}
}
try
{
var result = await CurrentIndexer.ResultsForQuery(CurrentQuery);
// Some trackers do not support multiple category filtering so filter the releases that match manually.
int? newItemCount = null;
// Cache non query results
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
{
newItemCount = cacheService.GetNewItemCount(CurrentIndexer, result.Releases);
cacheService.CacheRssResults(CurrentIndexer, result.Releases);
}
// Log info
var logBuilder = new StringBuilder();
if (newItemCount != null)
{
logBuilder.AppendFormat("Found {0} ({1} new) releases from {2}", result.Releases.Count(), newItemCount, CurrentIndexer.DisplayName);
}
else
{
logBuilder.AppendFormat("Found {0} releases from {1}", result.Releases.Count(), CurrentIndexer.DisplayName);
}
if (!string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
{
logBuilder.AppendFormat(" for: {0}", CurrentQuery.GetQueryString());
}
logger.Info(logBuilder.ToString());
var serverUrl = serverService.GetServerUrl(Request);
var resultPage = new ResultPage(new ChannelInfo
{
Title = CurrentIndexer.DisplayName,
Description = CurrentIndexer.DisplayDescription,
Link = new Uri(CurrentIndexer.SiteLink),
ImageUrl = new Uri(serverUrl + "logos/" + CurrentIndexer.ID + ".png"),
ImageTitle = CurrentIndexer.DisplayName,
ImageLink = new Uri(CurrentIndexer.SiteLink),
ImageDescription = CurrentIndexer.DisplayName
});
var proxiedReleases = result.Releases.Select(r => AutoMapper.Mapper.Map<ReleaseInfo>(r)).Select(r =>
{
r.Link = serverService.ConvertToProxyLink(r.Link, serverUrl, r.Origin.ID, "dl", r.Title);
return r;
});
resultPage.Releases = proxiedReleases.ToList();
var xml = resultPage.ToXml(new Uri(serverUrl));
// Force the return as XML
return Content(xml, "application/rss+xml", Encoding.UTF8);
}
catch (Exception ex)
{
logger.Error(ex);
return GetErrorXML(900, ex.ToString());
}
}
[Route("[action]/{ignored?}")]
public IActionResult GetErrorXML(int code, string description)
{
return Content(CreateErrorXML(code, description), "application/xml", Encoding.UTF8);
}
public static string CreateErrorXML(int code, string description)
{
var xdoc = new XDocument(
new XDeclaration("1.0", "UTF-8", null),
new XElement("error",
new XAttribute("code", code.ToString()),
new XAttribute("description", description)
)
);
return xdoc.Declaration + Environment.NewLine + xdoc;
}
public static IActionResult GetErrorActionResult(RouteData routeData, HttpStatusCode status, int torznabCode, string description)
{
bool isTorznab = routeData.Values["action"].ToString().Equals("torznab", StringComparison.OrdinalIgnoreCase);
if (isTorznab)
{
ContentResult contentResult = new ContentResult
{
Content = CreateErrorXML(torznabCode, description),
ContentType = "application/xml",
StatusCode = 200
};
return contentResult;
}
else
{
switch (status)
{
case HttpStatusCode.Unauthorized:
return new UnauthorizedResult();
case HttpStatusCode.NotFound:
return new NotFoundObjectResult(description);
case HttpStatusCode.BadRequest:
return new BadRequestObjectResult(description);
default:
return new ContentResult
{
Content = description,
StatusCode = (int)status
};
}
}
}
[Route("[action]/{ignored?}")]
[HttpGet]
public async Task<TorrentPotatoResponse> Potato([FromQuery]TorrentPotatoRequest request)
{
var result = await CurrentIndexer.ResultsForQuery(CurrentQuery);
// Cache non query results
if (string.IsNullOrEmpty(CurrentQuery.SanitizedSearchTerm))
cacheService.CacheRssResults(CurrentIndexer, result.Releases);
// Log info
if (string.IsNullOrWhiteSpace(CurrentQuery.SanitizedSearchTerm))
logger.Info($"Found {result.Releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName}");
else
logger.Info($"Found {result.Releases.Count()} torrentpotato releases from {CurrentIndexer.DisplayName} for: {CurrentQuery.GetQueryString()}");
var serverUrl = serverService.GetServerUrl(Request);
var potatoReleases = result.Releases.Where(r => r.Link != null || r.MagnetUri != null).Select(r =>
{
var release = AutoMapper.Mapper.Map<ReleaseInfo>(r);
release.Link = serverService.ConvertToProxyLink(release.Link, serverUrl, CurrentIndexer.ID, "dl", release.Title);
var item = new TorrentPotatoResponseItem()
{
release_name = release.Title + "[" + CurrentIndexer.DisplayName + "]", // Suffix the indexer so we can see which tracker we are using in CPS as it just says torrentpotato >.>
torrent_id = release.Guid.ToString(),
details_url = release.Comments.ToString(),
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
imdb_id = release.Imdb.HasValue ? ParseUtil.GetFullImdbID("tt" + release.Imdb) : null,
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
type = "movie",
size = (long)release.Size / (1024 * 1024), // This is in MB
leechers = (release.Peers ?? -1) - (release.Seeders ?? 0),
seeders = release.Seeders ?? -1,
publish_date = r.PublishDate == DateTime.MinValue ? null : release.PublishDate.ToUniversalTime().ToString("s")
};
return item;
});
var potatoResponse = new TorrentPotatoResponse()
{
results = potatoReleases.ToList()
};
return potatoResponse;
}
[Route("[action]/{ignored?}")]
private void ConfigureCacheResults(IEnumerable<TrackerCacheResult> results)
{
var serverUrl = serverService.GetServerUrl(Request);
foreach (var result in results)
{
var link = result.Link;
var file = StringUtil.MakeValidFileName(result.Title, '_', false);
result.Link = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "dl", file);
if (!string.IsNullOrWhiteSpace(serverConfig.BlackholeDir))
{
if (result.Link != null)
result.BlackholeLink = serverService.ConvertToProxyLink(link, serverUrl, result.TrackerId, "bh", file);
else if (result.MagnetUri != null)
result.BlackholeLink = serverService.ConvertToProxyLink(result.MagnetUri, serverUrl, result.TrackerId, "bh", file);
}
}
}
}
}

View 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;
}
}
}

View 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
});
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,163 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Jackett.Server.Services;
using NLog;
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace Jackett.Server
{
public static class Initialisation
{
public static void ProcessSettings(RuntimeSettings runtimeSettings, Logger logger)
{
if (runtimeSettings.ClientOverride != "httpclient" && runtimeSettings.ClientOverride != "httpclient2" && runtimeSettings.ClientOverride != "httpclientnetcore")
{
logger.Error($"Client override ({runtimeSettings.ClientOverride}) has been deprecated, please remove it from your start arguments");
Environment.Exit(1);
}
if (runtimeSettings.DoSSLFix != null)
{
logger.Error("SSLFix has been deprecated, please remove it from your start arguments");
Environment.Exit(1);
}
if (runtimeSettings.LogRequests)
{
logger.Info("Logging enabled.");
}
if (runtimeSettings.TracingEnabled)
{
logger.Info("Tracing enabled.");
}
if (runtimeSettings.IgnoreSslErrors == true)
{
logger.Error($"The IgnoreSslErrors option has been deprecated, please remove it from your start arguments");
}
if (!string.IsNullOrWhiteSpace(runtimeSettings.CustomDataFolder))
{
logger.Info("Jackett Data will be stored in: " + runtimeSettings.CustomDataFolder);
}
if (runtimeSettings.ProxyConnection != null)
{
logger.Info("Proxy enabled. " + runtimeSettings.ProxyConnection);
}
}
public static void ProcessWindowsSpecificArgs(ConsoleOptions consoleOptions, IProcessService processService, ServerConfig serverConfig, Logger logger)
{
IServiceConfigService serviceConfigService = new ServiceConfigService();
IServerService serverService = new ServerService(null, processService, null, null, logger, null, null, null, serverConfig);
/* ====== Actions ===== */
// Install service
if (consoleOptions.Install)
{
logger.Info("Initiating Jackett service install");
serviceConfigService.Install();
Environment.Exit(1);
}
// Uninstall service
if (consoleOptions.Uninstall)
{
logger.Info("Initiating Jackett service uninstall");
serverService.ReserveUrls(false);
serviceConfigService.Uninstall();
Environment.Exit(1);
}
// Start Service
if (consoleOptions.StartService)
{
if (!serviceConfigService.ServiceRunning())
{
logger.Info("Initiating Jackett service start");
serviceConfigService.Start();
}
Environment.Exit(1);
}
// Stop Service
if (consoleOptions.StopService)
{
if (serviceConfigService.ServiceRunning())
{
logger.Info("Initiating Jackett service stop");
serviceConfigService.Stop();
}
Environment.Exit(1);
}
// Reserve urls
if (consoleOptions.ReserveUrls)
{
logger.Info("Initiating ReserveUrls");
serverService.ReserveUrls(true);
Environment.Exit(1);
}
}
public static void ProcessConsoleOverrides(ConsoleOptions consoleOptions, IProcessService processService, ServerConfig serverConfig, IConfigurationService configurationService, Logger logger)
{
IServerService serverService = new ServerService(null, processService, null, null, logger, null, null, null, serverConfig);
// Override port
if (consoleOptions.Port != 0)
{
Int32.TryParse(serverConfig.Port.ToString(), out Int32 configPort);
if (configPort != consoleOptions.Port)
{
logger.Info("Overriding port to " + consoleOptions.Port);
serverConfig.Port = consoleOptions.Port;
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
if (ServerUtil.IsUserAdministrator())
{
serverService.ReserveUrls(true);
}
else
{
logger.Error("Unable to switch ports when not running as administrator");
Environment.Exit(1);
}
}
configurationService.SaveConfig(serverConfig);
}
}
// Override listen public
if (consoleOptions.ListenPublic || consoleOptions.ListenPrivate)
{
if (serverConfig.AllowExternal != consoleOptions.ListenPublic)
{
logger.Info("Overriding external access to " + consoleOptions.ListenPublic);
serverConfig.AllowExternal = consoleOptions.ListenPublic;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (ServerUtil.IsUserAdministrator())
{
serverService.ReserveUrls(true);
}
else
{
logger.Error("Unable to switch to public listening without admin rights.");
Environment.Exit(1);
}
}
configurationService.SaveConfig(serverConfig);
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<ApplicationIcon>jackett.ico</ApplicationIcon>
<AssemblyName>JackettConsole</AssemblyName>
<OutputType>Exe</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<RuntimeIdentifiers>win-x86;linux-x64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
<RuntimeIdentifiers>win7-x86;linux-x64</RuntimeIdentifiers>
</PropertyGroup>
<!-- Conditionally obtain references for the .NET Core App 2.1 target -->
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.8.1" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.2" />
<PackageReference Include="AutoMapper" Version="7.0.1" />
<PackageReference Include="CommandLineParser" Version="2.2.1" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="2.1.1" />
<PackageReference Include="NLog" Version="4.5.6" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.5.4" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\README.md" Visible="false">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\LICENSE" Visible="false">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\..\Upstart.config" Visible="false">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Jackett.Common;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
namespace Jackett.Server.Middleware
{
public class CustomExceptionHandler
{
private readonly RequestDelegate _next;
private Logger logger;
public CustomExceptionHandler(RequestDelegate next, Logger l)
{
_next = next;
logger = l;
}
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
try
{
string msg = "";
var json = new JObject();
logger.Error(ex);
var message = ex.Message;
if (ex.InnerException != null)
{
message += ": " + ex.InnerException.Message;
}
msg = message;
if (ex is ExceptionWithConfigData)
{
json["config"] = ((ExceptionWithConfigData)ex).ConfigData.ToJson(null, false);
}
json["result"] = "error";
json["error"] = msg;
json["stacktrace"] = ex.StackTrace;
if (ex.InnerException != null)
{
json["innerstacktrace"] = ex.InnerException.StackTrace;
}
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsync(json.ToString());
return;
}
catch (Exception ex2)
{
logger.Error(ex2, "An exception was thrown attempting to execute the custom exception error handler.");
}
await _next(httpContext);
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class CustomExceptionHandlerExtensions
{
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomExceptionHandler>();
}
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Net.Http.Headers;
using System;
namespace Jackett.Server.Middleware
{
public class RedirectRules
{
public static void RedirectToDashboard(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.Path == null || string.IsNullOrWhiteSpace(request.Path.ToString()) || request.Path.ToString() == "/"
|| request.Path.ToString().Equals("/index.html", StringComparison.OrdinalIgnoreCase))
{
// 301 is the status code of permanent redirect
var redir = Helper.ServerService.BasePath() + "/UI/Dashboard";
var response = context.HttpContext.Response;
response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Result = RuleResult.EndResponse;
response.Headers[HeaderNames.Location] = redir;
}
}
}
}

View File

@@ -0,0 +1,185 @@
using CommandLine;
using CommandLine.Text;
using Jackett.Common.Models.Config;
using Jackett.Common.Services;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using NLog;
using NLog.Web;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Jackett.Server
{
public static class Program
{
public static IConfiguration Configuration { get; set; }
private static RuntimeSettings Settings { get; set; }
public static bool isWebHostRestart = false;
public static void Main(string[] args)
{
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
var optionsResult = commandLineParser.ParseArguments<ConsoleOptions>(args);
var runtimeDictionary = new Dictionary<string, string>();
ConsoleOptions consoleOptions = new ConsoleOptions();
optionsResult.WithNotParsed(errors =>
{
var text = HelpText.AutoBuild(optionsResult);
text.Copyright = " ";
text.Heading = "Jackett v" + EnvironmentUtil.JackettVersion;
Console.WriteLine(text);
Environment.Exit(1);
return;
});
optionsResult.WithParsed(options =>
{
if (string.IsNullOrEmpty(options.Client))
{
//TODO: Remove libcurl once off owin
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("Core", StringComparison.OrdinalIgnoreCase) >= 0;
if (runningOnDotNetCore)
{
options.Client = "httpclientnetcore";
}
else
{
options.Client = "httpclient";
}
}
Settings = options.ToRunTimeSettings();
consoleOptions = options;
runtimeDictionary = GetValues(Settings);
});
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(Settings);
Logger logger = LogManager.GetCurrentClassLogger();
logger.Info("Starting Jackett v" + EnvironmentUtil.JackettVersion);
// create PID file early
if (!string.IsNullOrWhiteSpace(Settings.PIDFile))
{
try
{
var proc = Process.GetCurrentProcess();
File.WriteAllText(Settings.PIDFile, proc.Id.ToString());
}
catch (Exception e)
{
logger.Error(e, "Error while creating the PID file");
}
}
Initialisation.ProcessSettings(Settings, logger);
ISerializeService serializeService = new SerializeService();
IProcessService processService = new ProcessService(logger);
IConfigurationService configurationService = new ConfigurationService(serializeService, processService, logger, Settings);
if (consoleOptions.Install || consoleOptions.Uninstall || consoleOptions.StartService || consoleOptions.StopService || consoleOptions.ReserveUrls)
{
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
if (isWindows)
{
ServerConfig serverConfig = configurationService.BuildServerConfig(Settings);
Initialisation.ProcessWindowsSpecificArgs(consoleOptions, processService, serverConfig, logger);
}
else
{
logger.Error($"ReserveUrls and service arguments only apply to Windows, please remove them from your start arguments");
Environment.Exit(1);
}
}
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(runtimeDictionary);
Configuration = builder.Build();
do
{
if (!isWebHostRestart)
{
if (consoleOptions.Port != 0 || consoleOptions.ListenPublic || consoleOptions.ListenPrivate)
{
ServerConfig serverConfiguration = configurationService.BuildServerConfig(Settings);
Initialisation.ProcessConsoleOverrides(consoleOptions, processService, serverConfiguration, configurationService, logger);
}
}
ServerConfig serverConfig = configurationService.BuildServerConfig(Settings);
Int32.TryParse(serverConfig.Port.ToString(), out Int32 configPort);
string[] url = serverConfig.GetListenAddresses(serverConfig.AllowExternal).Take(1).ToArray(); //Kestrel doesn't need 127.0.0.1 and localhost to be registered, remove once off OWIN
isWebHostRestart = false;
try
{
logger.Debug("Creating web host...");
CreateWebHostBuilder(args, url).Build().Run();
}
catch (Exception ex)
{
if (ex.InnerException is Microsoft.AspNetCore.Connections.AddressInUseException)
{
logger.Error("Address already in use: Most likely Jackett is already running. " + ex.Message);
Environment.Exit(1);
}
logger.Error(ex);
throw;
}
} while (isWebHostRestart);
}
public static Dictionary<string, string> GetValues(object obj)
{
return obj
.GetType()
.GetProperties()
.ToDictionary(p => "RuntimeSettings:" + p.Name, p => p.GetValue(obj) == null ? null : p.GetValue(obj).ToString());
}
private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
try
{
if (Settings != null && !string.IsNullOrWhiteSpace(Settings.PIDFile))
{
var PIDFile = Settings.PIDFile;
if (File.Exists(PIDFile))
{
Console.WriteLine("Deleting PID file " + PIDFile);
File.Delete(PIDFile);
}
LogManager.Shutdown();
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString(), "Error while deleting the PID file");
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args, string[] urls) =>
WebHost.CreateDefaultBuilder(args)
.UseConfiguration(Configuration)
.UseUrls(urls)
.PreferHostingUrls(true)
.UseStartup<Startup>()
.UseNLog();
}
}

View File

@@ -0,0 +1,211 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Jackett.Common;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using Microsoft.AspNetCore.DataProtection;
namespace Jackett.Server.Services
{
public class ProtectionService : IProtectionService
{
DataProtectionScope PROTECTION_SCOPE = DataProtectionScope.LocalMachine;
private const string JACKETT_KEY = "JACKETT_KEY";
const string APPLICATION_KEY = "Dvz66r3n8vhTGip2/quiw5ISyM37f7L2iOdupzdKmzkvXGhAgQiWK+6F+4qpxjPVNks1qO7LdWuVqRlzgLzeW8mChC6JnBMUS1Fin4N2nS9lh4XPuCZ1che75xO92Nk2vyXUo9KSFG1hvEszAuLfG2Mcg1r0sVyVXd2gQDU/TbY=";
private byte[] _instanceKey;
IDataProtector _protector = null;
public ProtectionService(ServerConfig config, IDataProtectionProvider provider = null)
{
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
// We should not be running as root and will only have access to the local store.
PROTECTION_SCOPE = DataProtectionScope.CurrentUser;
}
_instanceKey = Encoding.UTF8.GetBytes(config.InstanceId);
if (provider != null)
{
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
string purpose = string.IsNullOrEmpty(jackettKey) ? APPLICATION_KEY : jackettKey.ToString();
_protector = provider.CreateProtector(purpose);
}
}
public string Protect(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
return _protector.Protect(plainText);
}
public string UnProtect(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
return _protector.Unprotect(plainText);
}
public string LegacyProtect(string plainText)
{
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
if (jackettKey == null)
{
return ProtectDefaultMethod(plainText);
}
else
{
return ProtectUsingKey(plainText, jackettKey);
}
}
public string LegacyUnProtect(string plainText)
{
var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY);
if (jackettKey == null)
{
return UnProtectDefaultMethod(plainText);
}
else
{
return UnProtectUsingKey(plainText, jackettKey);
}
}
private string ProtectDefaultMethod(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var appKey = Convert.FromBase64String(APPLICATION_KEY);
var instanceKey = _instanceKey;
var entropy = new byte[appKey.Length + instanceKey.Length];
Buffer.BlockCopy(instanceKey, 0, entropy, 0, instanceKey.Length);
Buffer.BlockCopy(appKey, 0, entropy, instanceKey.Length, appKey.Length);
var protectedBytes = ProtectedData.Protect(plainBytes, entropy, PROTECTION_SCOPE);
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(instanceKey, instanceKey.Reverse().ToArray(), 64);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(protectedBytes, 0, protectedBytes.Length);
cs.Close();
}
protectedBytes = ms.ToArray();
}
}
return Convert.ToBase64String(protectedBytes);
}
private string UnProtectDefaultMethod(string plainText)
{
if (string.IsNullOrEmpty(plainText))
return string.Empty;
var protectedBytes = Convert.FromBase64String(plainText);
var instanceKey = _instanceKey;
using (MemoryStream ms = new MemoryStream())
{
using (RijndaelManaged AES = new RijndaelManaged())
{
AES.KeySize = 256;
AES.BlockSize = 128;
var key = new Rfc2898DeriveBytes(instanceKey, instanceKey.Reverse().ToArray(), 64);
AES.Key = key.GetBytes(AES.KeySize / 8);
AES.IV = key.GetBytes(AES.BlockSize / 8);
AES.Mode = CipherMode.CBC;
using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(protectedBytes, 0, protectedBytes.Length);
cs.Close();
}
protectedBytes = ms.ToArray();
}
}
var appKey = Convert.FromBase64String(APPLICATION_KEY);
var entropy = new byte[appKey.Length + instanceKey.Length];
Buffer.BlockCopy(instanceKey, 0, entropy, 0, instanceKey.Length);
Buffer.BlockCopy(appKey, 0, entropy, instanceKey.Length, appKey.Length);
var unprotectedBytes = ProtectedData.Unprotect(protectedBytes, entropy, PROTECTION_SCOPE);
return Encoding.UTF8.GetString(unprotectedBytes);
}
private string ProtectUsingKey(string plainText, string key)
{
return StringCipher.Encrypt(plainText, key);
}
private string UnProtectUsingKey(string plainText, string key)
{
return StringCipher.Decrypt(plainText, key);
}
public void Protect<T>(T obj)
{
var type = obj.GetType();
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.Public))
{
if (property.GetCustomAttributes(typeof(JackettProtectedAttribute), false).Count() > 0)
{
var value = property.GetValue(obj);
if (value is string)
{
var protectedString = Protect(value as string);
property.SetValue(obj, protectedString);
}
}
}
}
public void UnProtect<T>(T obj)
{
var type = obj.GetType();
foreach (var property in type.GetProperties(BindingFlags.SetProperty | BindingFlags.GetProperty | BindingFlags.Public))
{
if (property.GetCustomAttributes(typeof(JackettProtectedAttribute), false).Count() > 0)
{
var value = property.GetValue(obj);
if (value is string)
{
var unprotectedString = UnProtect(value as string);
property.SetValue(obj, unprotectedString);
}
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
namespace Jackett.Server.Services
{
class SecuityService : ISecuityService
{
private const string COOKIENAME = "JACKETT";
private ServerConfig _serverConfig;
public SecuityService(ServerConfig sc)
{
_serverConfig = sc;
}
public string HashPassword(string input)
{
if (input == null)
return null;
// Append key as salt
input += _serverConfig.APIKey;
UnicodeEncoding UE = new UnicodeEncoding();
byte[] hashValue;
byte[] message = UE.GetBytes(input);
SHA512Managed hashString = new SHA512Managed();
string hex = "";
hashValue = hashString.ComputeHash(message);
foreach (byte x in hashValue)
{
hex += String.Format("{0:x2}", x);
}
return hex;
}
public void Login(HttpResponseMessage response)
{
// Login
response.Headers.Add("Set-Cookie", COOKIENAME + "=" + _serverConfig.AdminPassword + "; path=/");
}
public void Logout(HttpResponseMessage response)
{
// Logout
response.Headers.Add("Set-Cookie", COOKIENAME + "=; path=/");
}
public bool CheckAuthorised(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(_serverConfig.AdminPassword))
return true;
try
{
var cookie = request.Headers.GetValues(COOKIENAME).FirstOrDefault();
if (cookie != null)
{
return cookie == _serverConfig.AdminPassword;
}
}
catch { }
return false;
}
}
}

View File

@@ -0,0 +1,341 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using NLog;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace Jackett.Server.Services
{
public class ServerService : IServerService
{
private IIndexerManagerService indexerService;
private IProcessService processService;
private ISerializeService serializeService;
private IConfigurationService configService;
private Logger logger;
private Common.Utils.Clients.WebClient client;
private IUpdateService updater;
private List<string> _notices = new List<string>();
private ServerConfig config;
private IProtectionService _protectionService;
public ServerService(IIndexerManagerService i, IProcessService p, ISerializeService s, IConfigurationService c, Logger l, Common.Utils.Clients.WebClient w, IUpdateService u, IProtectionService protectionService, ServerConfig serverConfig)
{
indexerService = i;
processService = p;
serializeService = s;
configService = c;
logger = l;
client = w;
updater = u;
config = serverConfig;
_protectionService = protectionService;
}
public List<string> notices
{
get
{
return _notices;
}
}
public Uri ConvertToProxyLink(Uri link, string serverUrl, string indexerId, string action = "dl", string file = "t")
{
if (link == null || (link.IsAbsoluteUri && link.Scheme == "magnet" && action != "bh")) // no need to convert a magnet link to a proxy link unless it's a blackhole link
return link;
var encryptedLink = _protectionService.Protect(link.ToString());
var encodedLink = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(encryptedLink));
string urlEncodedFile = WebUtility.UrlEncode(file);
var proxyLink = string.Format("{0}{1}/{2}/?jackett_apikey={3}&path={4}&file={5}", serverUrl, action, indexerId, config.APIKey, encodedLink, urlEncodedFile);
return new Uri(proxyLink);
}
public string BasePath()
{
if (config.BasePathOverride == null || config.BasePathOverride == "")
{
return "";
}
var path = config.BasePathOverride;
if (path.EndsWith("/"))
{
path = path.TrimEnd('/');
}
if (!path.StartsWith("/"))
{
path = "/" + path;
}
return path;
}
public void Initalize()
{
try
{
var x = Environment.OSVersion;
var runtimedir = RuntimeEnvironment.GetRuntimeDirectory();
logger.Info("Environment version: " + Environment.Version.ToString() + " (" + runtimedir + ")");
logger.Info("OS version: " + Environment.OSVersion.ToString() + (Environment.Is64BitOperatingSystem ? " (64bit OS)" : "") + (Environment.Is64BitProcess ? " (64bit process)" : ""));
try
{
ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads);
logger.Info("ThreadPool MaxThreads: " + workerThreads + " workerThreads, " + completionPortThreads + " completionPortThreads");
}
catch (Exception e)
{
logger.Error("Error while getting MaxThreads details: " + e);
}
logger.Info("App config/log directory: " + configService.GetAppDataFolder());
try
{
var issuefile = "/etc/issue";
if (File.Exists(issuefile))
{
using (StreamReader reader = new StreamReader(issuefile))
{
string firstLine = reader.ReadLine();
if (firstLine != null)
logger.Info("issue: " + firstLine);
}
}
}
catch (Exception e)
{
logger.Error(e, "Error while reading the issue file");
}
bool runningOnDotNetCore = RuntimeInformation.FrameworkDescription.IndexOf("Core", StringComparison.OrdinalIgnoreCase) >= 0;
Type monotype = Type.GetType("Mono.Runtime");
if (monotype != null && !runningOnDotNetCore)
{
MethodInfo displayName = monotype.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
var monoVersion = "unknown";
if (displayName != null)
monoVersion = displayName.Invoke(null, null).ToString();
logger.Info("mono version: " + monoVersion);
var monoVersionO = new Version(monoVersion.Split(' ')[0]);
if (monoVersionO.Major < 5 || (monoVersionO.Major == 5 && monoVersionO.Minor < 8))
{
//Hard minimum of 5.8
//5.4 throws a SIGABRT, looks related to this which was fixed in 5.8 https://bugzilla.xamarin.com/show_bug.cgi?id=60625
logger.Error("Your mono version is too old. Please update to the latest version from http://www.mono-project.com/download/");
Environment.Exit(2);
}
if (monoVersionO.Major < 5 || (monoVersionO.Major == 5 && monoVersionO.Minor < 8))
{
string notice = "A minimum Mono version of 5.8 is required. Please update to the latest version from http://www.mono-project.com/download/";
_notices.Add(notice);
logger.Error(notice);
}
try
{
// Check for mono-devel
// Is there any better way which doesn't involve a hard cashes?
var mono_devel_file = Path.Combine(runtimedir, "mono-api-info.exe");
if (!File.Exists(mono_devel_file))
{
var notice = "It looks like the mono-devel package is not installed, please make sure it's installed to avoid crashes.";
_notices.Add(notice);
logger.Error(notice);
}
}
catch (Exception e)
{
logger.Error(e, "Error while checking for mono-devel");
}
try
{
// Check for ca-certificates-mono
var mono_cert_file = Path.Combine(runtimedir, "cert-sync.exe");
if (!File.Exists(mono_cert_file))
{
var notice = "The ca-certificates-mono package is not installed, HTTPS trackers won't work. Please install it.";
_notices.Add(notice);
logger.Error(notice);
}
}
catch (Exception e)
{
logger.Error(e, "Error while checking for ca-certificates-mono");
}
try
{
Encoding.GetEncoding("windows-1255");
}
catch (NotSupportedException e)
{
logger.Debug(e);
logger.Error(e.Message + " Most likely the mono-locale-extras package is not installed.");
Environment.Exit(2);
}
// check if the certificate store was initialized using Mono.Security.X509.X509StoreManager.TrustedRootCertificates.Count
try
{
var monoSecurity = Assembly.Load("Mono.Security");
Type monoX509StoreManager = monoSecurity.GetType("Mono.Security.X509.X509StoreManager");
if (monoX509StoreManager != null)
{
var TrustedRootCertificatesProperty = monoX509StoreManager.GetProperty("TrustedRootCertificates");
var TrustedRootCertificates = (ICollection)TrustedRootCertificatesProperty.GetValue(null);
logger.Info("TrustedRootCertificates count: " + TrustedRootCertificates.Count);
if (TrustedRootCertificates.Count == 0)
{
var CACertificatesFiles = new string[] {
"/etc/ssl/certs/ca-certificates.crt", // Debian based
"/etc/pki/tls/certs/ca-bundle.c", // RedHat based
"/etc/ssl/ca-bundle.pem", // SUSE
};
var notice = "The mono certificate store is not initialized.<br/>\n";
var logSpacer = " ";
var CACertificatesFile = CACertificatesFiles.Where(f => File.Exists(f)).FirstOrDefault();
var CommandRoot = "curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync /dev/stdin";
var CommandUser = "curl -sS https://curl.haxx.se/ca/cacert.pem | cert-sync --user /dev/stdin";
if (CACertificatesFile != null)
{
CommandRoot = "cert-sync " + CACertificatesFile;
CommandUser = "cert-sync --user " + CACertificatesFile;
}
notice += logSpacer + "Please run the following command as root:<br/>\n";
notice += logSpacer + "<pre>" + CommandRoot + "</pre><br/>\n";
notice += logSpacer + "If you don't have root access or you're running MacOS, please run the following command as the jackett user (" + Environment.UserName + "):<br/>\n";
notice += logSpacer + "<pre>" + CommandUser + "</pre>";
_notices.Add(notice);
logger.Error(Regex.Replace(notice, "<.*?>", String.Empty));
}
}
}
catch (Exception e)
{
logger.Error(e, "Error while chekcing the mono certificate store");
}
}
}
catch (Exception e)
{
logger.Error("Error while getting environment details: " + e);
}
try
{
if (Environment.UserName == "root")
{
var notice = "Jackett is running with root privileges. You should run Jackett as an unprivileged user.";
_notices.Add(notice);
logger.Error(notice);
}
}
catch (Exception e)
{
logger.Error(e, "Error while checking the username");
}
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
// Load indexers
indexerService.InitIndexers(configService.GetCardigannDefinitionsFolders());
client.Init();
updater.CleanupTempDir();
}
public void Start()
{
updater.StartUpdateChecker();
}
public void ReserveUrls(bool doInstall = true)
{
logger.Debug("Unreserving Urls");
config.GetListenAddresses(false).ToList().ForEach(u => RunNetSh(string.Format("http delete urlacl {0}", u)));
config.GetListenAddresses(true).ToList().ForEach(u => RunNetSh(string.Format("http delete urlacl {0}", u)));
if (doInstall)
{
logger.Debug("Reserving Urls");
config.GetListenAddresses(true).ToList().ForEach(u => RunNetSh(string.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", u)));
logger.Debug("Urls reserved");
}
}
private void RunNetSh(string args)
{
processService.StartProcessAndLog("netsh.exe", args);
}
public void Stop()
{
// Only needed for Owin
}
public string GetServerUrl(Object obj)
{
string serverUrl = "";
if (obj is HttpRequest request)
{
var scheme = request.Scheme;
var port = request.HttpContext.Request.Host.Port;
// Check for protocol headers added by reverse proxys
// X-Forwarded-Proto: A de facto standard for identifying the originating protocol of an HTTP request
var X_Forwarded_Proto = request.Headers.Where(x => x.Key == "X-Forwarded-Proto").Select(x => x.Value).FirstOrDefault();
if (X_Forwarded_Proto.Count > 0)
{
scheme = X_Forwarded_Proto.First();
}
// Front-End-Https: Non-standard header field used by Microsoft applications and load-balancers
else if (request.Headers.Where(x => x.Key == "Front-End-Https" && x.Value.FirstOrDefault() == "on").Any())
{
scheme = "https";
}
//default to 443 if the Host header doesn't contain the port (needed for reverse proxy setups)
if (scheme == "https" && !request.HttpContext.Request.Host.Value.Contains(":"))
{
port = 443;
}
serverUrl = string.Format("{0}://{1}:{2}{3}/", scheme, request.HttpContext.Request.Host.Host, port, BasePath());
}
return serverUrl;
}
public string GetBlackholeDirectory()
{
return config.BlackholeDir;
}
public string GetApiKey()
{
return config.APIKey;
}
}
}

View File

@@ -0,0 +1,121 @@
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using Jackett.Common.Services.Interfaces;
using System.Reflection;
using Jackett.Common.Services;
namespace Jackett.Server.Services
{
public class ServiceConfigService : IServiceConfigService
{
private const string NAME = "Jackett";
private const string DESCRIPTION = "API Support for your favorite torrent trackers";
private const string SERVICEEXE = "JackettService.exe";
private IProcessService processService;
private Logger logger;
public ServiceConfigService()
{
logger = LogManager.GetCurrentClassLogger();
processService = new ProcessService(logger);
}
public bool ServiceExists()
{
return GetService(NAME) != null;
}
public bool ServiceRunning()
{
var service = GetService(NAME);
if (service == null)
return false;
return service.Status == ServiceControllerStatus.Running;
}
public void Start()
{
var service = GetService(NAME);
service.Start();
}
public void Stop()
{
var service = GetService(NAME);
service.Stop();
}
public ServiceController GetService(string serviceName)
{
return ServiceController.GetServices().FirstOrDefault(c => String.Equals(c.ServiceName, serviceName, StringComparison.InvariantCultureIgnoreCase));
}
public void Install()
{
if (ServiceExists())
{
logger.Warn("The service is already installed!");
}
else
{
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
var exePath = Path.Combine(applicationFolder, SERVICEEXE);
if (!File.Exists(exePath) && Debugger.IsAttached)
{
exePath = Path.Combine(applicationFolder, "..\\..\\..\\Jackett.Service\\bin\\Debug", SERVICEEXE);
}
string arg = $"create {NAME} start= auto binpath= \"{exePath}\" DisplayName= {NAME}";
processService.StartProcessAndLog("sc.exe", arg, true);
processService.StartProcessAndLog("sc.exe", $"description {NAME} \"{DESCRIPTION}\"", true);
}
}
public void Uninstall()
{
RemoveService();
processService.StartProcessAndLog("sc.exe", $"delete {NAME}", true);
logger.Info("The service was uninstalled.");
}
public void RemoveService()
{
var service = GetService(NAME);
if(service == null)
{
logger.Warn("The service is already uninstalled");
return;
}
if (service.Status != ServiceControllerStatus.Stopped)
{
service.Stop();
service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(60));
service.Refresh();
if (service.Status == ServiceControllerStatus.Stopped)
{
logger.Info("Service stopped.");
}
else
{
logger.Error("Failed to stop the service");
}
}
else
{
logger.Warn("The service was already stopped");
}
}
}
}

View File

@@ -0,0 +1,148 @@
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Jackett.Common.Models.Config;
using Jackett.Common.Plumbing;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils.Clients;
using Jackett.Server.Middleware;
using Jackett.Server.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json.Serialization;
using System;
using System.IO;
using System.Text;
namespace Jackett.Server
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddResponseCompression();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/UI/Login");
options.AccessDeniedPath = new PathString("/UI/Login");
options.LogoutPath = new PathString("/UI/Logout");
options.Cookie.Name = "Jackett";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver(); //Web app uses Pascal Case JSON
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
RuntimeSettings runtimeSettings = new RuntimeSettings();
Configuration.GetSection("RuntimeSettings").Bind(runtimeSettings);
DirectoryInfo dataProtectionFolder = new DirectoryInfo(Path.Combine(runtimeSettings.DataFolder, "DataProtection"));
services.AddDataProtection()
.PersistKeysToFileSystem(dataProtectionFolder)
.SetApplicationName("Jackett");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var builder = new ContainerBuilder();
Helper.SetupLogging(builder);
builder.Populate(services);
builder.RegisterModule(new JackettModule(runtimeSettings));
builder.RegisterType<SecuityService>().As<ISecuityService>();
builder.RegisterType<ServerService>().As<IServerService>();
builder.RegisterType<ProtectionService>().As<IProtectionService>();
builder.RegisterType<ServiceConfigService>().As<IServiceConfigService>();
if (runtimeSettings.ClientOverride == "httpclientnetcore")
builder.RegisterType<HttpWebClientNetCore>().As<WebClient>();
IContainer container = builder.Build();
Helper.ApplicationContainer = container;
Helper.Logger.Debug("Autofac container built");
Helper.Initialize();
return new AutofacServiceProvider(container);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
{
applicationLifetime.ApplicationStopping.Register(OnShutdown);
Helper.applicationLifetime = applicationLifetime;
app.UseResponseCompression();
app.UseDeveloperExceptionPage();
app.UseCustomExceptionHandler();
string serverBasePath = Helper.ServerService.BasePath() ?? string.Empty;
if (!string.IsNullOrEmpty(serverBasePath))
{
app.UsePathBase(serverBasePath);
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
var rewriteOptions = new RewriteOptions()
.AddRewrite(@"^torznab\/([\w-]*)", "api/v2.0/indexers/$1/results/torznab", skipRemainingRules: true) //legacy torznab route
.AddRewrite(@"^potato\/([\w-]*)", "api/v2.0/indexers/$1/results/potato", skipRemainingRules: true) //legacy potato route
.Add(RedirectRules.RedirectToDashboard);
app.UseRewriter(rewriteOptions);
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(Helper.ConfigService.GetContentFolder()),
RequestPath = "",
EnableDefaultFiles = true,
EnableDirectoryBrowsing = false
});
app.UseAuthentication();
app.UseMvc();
}
private void OnShutdown()
{
//this code is called when the application stops
}
}
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
</startup>
<system.net>
<settings>
<!-- needed to make the broken incapsula DDoS protection work on windows(e.g. for KickAssTorrent), see https://social.technet.microsoft.com/Forums/de-DE/b10b16d1-8eea-4b52-8aeb-f96ea87135fa/sectionresponseheader-detailcr-must-be-followed-by-lf?forum=powerquery -->
<httpWebRequest useUnsafeHeaderParsing="true" />
</settings>
</system.net>
</configuration>

View File

@@ -9,11 +9,12 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Jackett.Service</RootNamespace>
<AssemblyName>JackettService</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<RuntimeIdentifier>win</RuntimeIdentifier>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -65,25 +66,14 @@
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Content Include="jackett.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CurlSharp\CurlSharp.csproj">
<Project>{74420a79-cc16-442c-8b1e-7c1b913844f0}</Project>
<Name>CurlSharp</Name>
</ProjectReference>
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj">
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
<Name>Jackett.Common</Name>
</ProjectReference>
<ProjectReference Include="..\Jackett\Jackett.csproj">
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
<Name>Jackett</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@@ -1,29 +1,102 @@
using Jackett.Common.Models.Config;
using Jackett.Common.Services;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using Jackett.Common;
namespace Jackett.Service
{
public partial class Service : ServiceBase
{
private IProcessService processService;
private Process consoleProcess;
private Logger logger;
private bool serviceStopInitiated;
public Service()
{
InitializeComponent();
RuntimeSettings runtimeSettings = new RuntimeSettings()
{
CustomLogFileName = "ServiceLog.txt"
};
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
logger = LogManager.GetCurrentClassLogger();
logger.Info("Initiating Jackett Service v" + EnvironmentUtil.JackettVersion);
processService = new ProcessService(logger);
}
protected override void OnStart(string[] args)
{
Engine.BuildContainer(new RuntimeSettings(), new WebApi2Module());
Engine.Logger.Info("Service starting");
Engine.Server.Initalize();
Engine.Server.Start();
Engine.Logger.Info("Service started");
logger.Info("Service starting");
serviceStopInitiated = false;
StartConsoleApplication();
}
protected override void OnStop()
{
Engine.Logger.Info("Service stopping");
Engine.Server.Stop();
logger.Info("Service stopping");
serviceStopInitiated = true;
StopConsoleApplication();
}
private void StartConsoleApplication()
{
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
var exePath = Path.Combine(applicationFolder, "JackettConsole.exe");
var startInfo = new ProcessStartInfo()
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = exePath,
RedirectStandardInput = true,
RedirectStandardError = true
};
consoleProcess = Process.Start(startInfo);
consoleProcess.EnableRaisingEvents = true;
consoleProcess.Exited += ProcessExited;
consoleProcess.ErrorDataReceived += ProcessErrorDataReceived;
}
private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
{
logger.Error(e.Data);
}
private void ProcessExited(object sender, EventArgs e)
{
logger.Info("Console process exited");
if (!serviceStopInitiated)
{
logger.Info("Service stop not responsible for process exit");
Stop();
}
}
private void StopConsoleApplication()
{
if (consoleProcess != null && !consoleProcess.HasExited)
{
consoleProcess.StandardInput.Close();
consoleProcess.WaitForExit(2000);
if (consoleProcess != null && !consoleProcess.HasExited)
{
consoleProcess.Kill();
}
}
}
}
}

View File

@@ -1,37 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net452</TargetFramework>
<TargetFramework>net461</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Indexers\**" />
<EmbeddedResource Remove="Indexers\**" />
<None Remove="Indexers\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Util\Invalid-RSS.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Util\Invalid-RSS.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.8.1" />
<PackageReference Include="FluentAssertions" Version="5.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
<PackageReference Include="FluentAssertions" Version="5.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="NUnit.ConsoleRunner" Version="3.8.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
<ProjectReference Include="..\Jackett\Jackett.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
</ItemGroup>
</Project>

View File

@@ -18,7 +18,7 @@ namespace Jackett.Test
{
var builder = new ContainerBuilder();
builder.RegisterModule(new JackettModule(new RuntimeSettings()));
builder.RegisterModule<WebApi2Module>();
builder.RegisterType<Jackett.Services.ProtectionService>().As<IProtectionService>();
builder.RegisterType<TestWebClient>().As<WebClient>().SingleInstance();
builder.RegisterInstance(LogManager.GetCurrentClassLogger()).SingleInstance();
builder.RegisterType<TestIndexerManagerServiceHelper>().As<IIndexerManagerService>().SingleInstance();

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
</startup>
</configuration>

View File

@@ -9,11 +9,12 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Jackett.Tray</RootNamespace>
<AssemblyName>JackettTray</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<RuntimeIdentifier>win</RuntimeIdentifier>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -65,6 +66,7 @@
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TrayConsoleOptions.cs" />
<EmbeddedResource Include="Main.resx">
<DependentUpon>Main.cs</DependentUpon>
</EmbeddedResource>
@@ -88,11 +90,6 @@
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="jackett.ico" />
</ItemGroup>
@@ -101,10 +98,6 @@
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
<Name>Jackett.Common</Name>
</ProjectReference>
<ProjectReference Include="..\Jackett\Jackett.csproj">
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
<Name>Jackett</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<COMReference Include="IWshRuntimeLibrary">
@@ -117,6 +110,11 @@
<EmbedInteropTypes>True</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser">
<Version>2.2.1</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -60,51 +60,51 @@
this.toolStripMenuItemAutoStart,
this.toolStripMenuItemShutdown});
this.contextMenuStrip1.Name = "contextMenuStrip1";
this.contextMenuStrip1.Size = new System.Drawing.Size(292, 148);
this.contextMenuStrip1.Size = new System.Drawing.Size(296, 126);
this.contextMenuStrip1.Opening += new System.ComponentModel.CancelEventHandler(this.contextMenuStrip1_Opening);
//
// toolStripMenuItemWebUI
//
this.toolStripMenuItemWebUI.Name = "toolStripMenuItemWebUI";
this.toolStripMenuItemWebUI.Size = new System.Drawing.Size(291, 22);
this.toolStripMenuItemWebUI.Size = new System.Drawing.Size(295, 22);
this.toolStripMenuItemWebUI.Text = "Open Web UI";
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(288, 6);
this.toolStripSeparator1.Size = new System.Drawing.Size(292, 6);
//
// backgroundMenuItem
//
this.backgroundMenuItem.Enabled = false;
this.backgroundMenuItem.Name = "backgroundMenuItem";
this.backgroundMenuItem.Size = new System.Drawing.Size(291, 22);
this.backgroundMenuItem.Text = "Jacket is running as a background service";
this.backgroundMenuItem.Size = new System.Drawing.Size(295, 22);
this.backgroundMenuItem.Text = "Jackett is running as a background service";
//
// serviceControlMenuItem
//
this.serviceControlMenuItem.Name = "serviceControlMenuItem";
this.serviceControlMenuItem.Size = new System.Drawing.Size(291, 22);
this.serviceControlMenuItem.Size = new System.Drawing.Size(295, 22);
this.serviceControlMenuItem.Text = "Start Service";
this.serviceControlMenuItem.Click += new System.EventHandler(this.serviceControlMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(288, 6);
this.toolStripSeparator2.Size = new System.Drawing.Size(292, 6);
//
// toolStripMenuItemAutoStart
//
this.toolStripMenuItemAutoStart.CheckOnClick = true;
this.toolStripMenuItemAutoStart.Name = "toolStripMenuItemAutoStart";
this.toolStripMenuItemAutoStart.Size = new System.Drawing.Size(291, 22);
this.toolStripMenuItemAutoStart.Size = new System.Drawing.Size(295, 22);
this.toolStripMenuItemAutoStart.Text = "Auto-start on boot";
this.toolStripMenuItemAutoStart.Visible = false;
//
// toolStripMenuItemShutdown
//
this.toolStripMenuItemShutdown.Name = "toolStripMenuItemShutdown";
this.toolStripMenuItemShutdown.Size = new System.Drawing.Size(291, 22);
this.toolStripMenuItemShutdown.Size = new System.Drawing.Size(295, 22);
this.toolStripMenuItemShutdown.Text = "Shutdown";
//
// Main

View File

@@ -1,32 +1,52 @@
using System;
using Jackett.Common.Models.Config;
using Jackett.Common.Services;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using NLog;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using Jackett.Common;
using Jackett.Common.Models.Config;
using Jackett.Common.Utils;
using Microsoft.Win32;
using Jackett;
using Jackett.Utils;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
namespace Jackett.Tray
{
public partial class Main : Form
{
public Main()
private IProcessService processService;
private IServiceConfigService windowsService;
private ITrayLockService trayLockService;
private ISerializeService serializeService;
private IConfigurationService configurationService;
private ServerConfig serverConfig;
private Process consoleProcess;
private Logger logger;
private bool closeApplicationInitiated;
public Main(string updatedVersion)
{
Hide();
InitializeComponent();
RuntimeSettings runtimeSettings = new RuntimeSettings()
{
CustomLogFileName = "TrayLog.txt"
};
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
logger = LogManager.GetCurrentClassLogger();
logger.Info("Starting Jackett Tray v" + EnvironmentUtil.JackettVersion);
processService = new ProcessService(logger);
windowsService = new WindowsServiceConfigService(processService, logger);
trayLockService = new TrayLockService();
serializeService = new SerializeService();
configurationService = new ConfigurationService(serializeService, processService, logger, runtimeSettings);
serverConfig = configurationService.BuildServerConfig(runtimeSettings);
toolStripMenuItemAutoStart.Checked = AutoStart;
toolStripMenuItemAutoStart.CheckedChanged += toolStripMenuItemAutoStart_CheckedChanged;
@@ -34,18 +54,26 @@ namespace Jackett.Tray
toolStripMenuItemShutdown.Click += toolStripMenuItemShutdown_Click;
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
{
toolStripMenuItemAutoStart.Visible = true;
}
Engine.BuildContainer(new RuntimeSettings(),new WebApi2Module());
Engine.Server.Initalize();
if (!Engine.ServiceConfig.ServiceExists())
if (!windowsService.ServiceExists())
{
// We are not installed as a service so just the web server too and run from the tray.
Engine.Logger.Info("Starting server from tray");
Engine.Server.Start();
// We are not installed as a service so just start the web server via JackettConsole and run from the tray.
logger.Info("Starting server from tray");
StartConsoleApplication();
}
updatedVersion = updatedVersion.Equals("yes", StringComparison.OrdinalIgnoreCase) ? EnvironmentUtil.JackettVersion : updatedVersion;
if (!string.IsNullOrWhiteSpace(updatedVersion))
{
notifyIcon1.BalloonTipTitle = "Jackett";
notifyIcon1.BalloonTipText = $"Jackett has updated to version {updatedVersion}";
notifyIcon1.BalloonTipIcon = ToolTipIcon.Info;
notifyIcon1.ShowBalloonTip(10000);
logger.Info($"Display balloon tip, updated to {updatedVersion}");
}
Task.Factory.StartNew(WaitForEvent);
@@ -53,26 +81,48 @@ namespace Jackett.Tray
private void WaitForEvent()
{
Engine.LockService.WaitForSignal();
Application.Exit();
trayLockService.WaitForSignal();
logger.Info("Received signal from tray lock service");
if (windowsService.ServiceExists() && windowsService.ServiceRunning())
{
//We won't be able to start the tray app up again from the updater, as when running via a windows service there is no interaction with the desktop
//Fire off a console process that will start the tray 20 seconds later
string trayExePath = Assembly.GetEntryAssembly().Location;
var startInfo = new ProcessStartInfo()
{
Arguments = $"/c timeout 20 > NUL & \"{trayExePath}\" --UpdatedVersion yes",
FileName = "cmd.exe",
UseShellExecute = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
logger.Info("Starting 20 second delay tray launch as Jackett is running as a Windows service: " + startInfo.FileName + " " + startInfo.Arguments);
Process.Start(startInfo);
}
CloseTrayApplication();
}
void toolStripMenuItemWebUI_Click(object sender, EventArgs e)
private void toolStripMenuItemWebUI_Click(object sender, EventArgs e)
{
Process.Start("http://127.0.0.1:" + Engine.ServerConfig.Port);
Process.Start("http://127.0.0.1:" + serverConfig.Port);
}
void toolStripMenuItemShutdown_Click(object sender, EventArgs e)
private void toolStripMenuItemShutdown_Click(object sender, EventArgs e)
{
Process.GetCurrentProcess().Kill();
CloseTrayApplication();
}
void toolStripMenuItemAutoStart_CheckedChanged(object sender, EventArgs e)
private void toolStripMenuItemAutoStart_CheckedChanged(object sender, EventArgs e)
{
AutoStart = toolStripMenuItemAutoStart.Checked;
}
string ProgramTitle
private string ProgramTitle
{
get
{
@@ -80,7 +130,7 @@ namespace Jackett.Tray
}
}
bool AutoStart
private bool AutoStart
{
get
{
@@ -122,22 +172,29 @@ namespace Jackett.Tray
private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
{
if (Engine.ServiceConfig.ServiceExists())
if (windowsService.ServiceExists())
{
backgroundMenuItem.Visible = true;
serviceControlMenuItem.Visible = true;
toolStripSeparator1.Visible = true;
toolStripSeparator2.Visible = true;
if (Engine.ServiceConfig.ServiceRunning())
if (windowsService.ServiceRunning())
{
serviceControlMenuItem.Text = "Stop background service";
} else
backgroundMenuItem.Text = "Jackett is running as a background service";
toolStripMenuItemWebUI.Enabled = true;
}
else
{
serviceControlMenuItem.Text = "Start background service";
backgroundMenuItem.Text = "Jackett will run as a background service";
toolStripMenuItemWebUI.Enabled = false;
}
toolStripMenuItemShutdown.Text = "Close tray icon";
} else
}
else
{
backgroundMenuItem.Visible = false;
serviceControlMenuItem.Visible = false;
@@ -151,17 +208,17 @@ namespace Jackett.Tray
{
var consolePath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "JackettConsole.exe");
if (Engine.ServiceConfig.ServiceRunning())
if (windowsService.ServiceRunning())
{
if (ServerUtil.IsUserAdministrator())
{
Engine.ServiceConfig.Stop();
} else
windowsService.Stop();
}
else
{
try
{
Engine.ProcessService.StartProcessAndLog(consolePath, "--Stop", true);
processService.StartProcessAndLog(consolePath, "--Stop", true);
}
catch
{
@@ -173,13 +230,13 @@ namespace Jackett.Tray
{
if (ServerUtil.IsUserAdministrator())
{
Engine.ServiceConfig.Start();
windowsService.Start();
}
else
{
try
{
Engine.ProcessService.StartProcessAndLog(consolePath, "--Start", true);
processService.StartProcessAndLog(consolePath, "--Start", true);
}
catch
{
@@ -188,5 +245,64 @@ namespace Jackett.Tray
}
}
}
private void CloseTrayApplication()
{
closeApplicationInitiated = true;
logger.Info("Close of tray application initiated");
//Clears notify icon, otherwise icon will still appear on taskbar until you hover the mouse over
notifyIcon1.Icon = null;
notifyIcon1.Dispose();
Application.DoEvents();
if (consoleProcess != null && !consoleProcess.HasExited)
{
consoleProcess.StandardInput.Close();
System.Threading.Thread.Sleep(1000);
if (consoleProcess != null && !consoleProcess.HasExited)
{
consoleProcess.Kill();
}
}
Application.Exit();
}
private void StartConsoleApplication()
{
string applicationFolder = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
var exePath = Path.Combine(applicationFolder, "JackettConsole.exe");
var startInfo = new ProcessStartInfo()
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = exePath,
RedirectStandardInput = true,
RedirectStandardError = true
};
consoleProcess = Process.Start(startInfo);
consoleProcess.EnableRaisingEvents = true;
consoleProcess.Exited += ProcessExited;
consoleProcess.ErrorDataReceived += ProcessErrorDataReceived;
}
private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
{
logger.Error(e.Data);
}
private void ProcessExited(object sender, EventArgs e)
{
if (!closeApplicationInitiated)
{
logger.Info("Tray icon not responsible for process exit");
CloseTrayApplication();
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
using System;
using CommandLine;
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;
@@ -11,7 +12,7 @@ namespace Jackett.Tray
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
static void Main(string[] args)
{
var JacketTrayProcess = Process.GetCurrentProcess();
var runningProcesses = Process.GetProcesses();
@@ -23,10 +24,29 @@ namespace Jackett.Tray
MessageBox.Show("JackettTray is already running");
}
else
{
{
string newVersion = "";
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
try
{
var optionsResult = commandLineParser.ParseArguments<TrayConsoleOptions>(args);
optionsResult.WithParsed(options =>
{
if (!string.IsNullOrWhiteSpace(options.UpdatedVersion))
{
newVersion = options.UpdatedVersion;
}
});
}
catch (Exception e)
{
newVersion = "";
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Main());
Application.Run(new Main(newVersion));
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Jackett.Tray.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.3.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));

View File

@@ -0,0 +1,10 @@
using CommandLine;
namespace Jackett.Tray
{
public class TrayConsoleOptions
{
[Option("UpdatedVersion", HelpText = "Indicates the new version that Jackett just updated to so that user understands why they are getting a prompt to start Windows service")]
public string UpdatedVersion { get; set; }
}
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
</startup>
</configuration>

View File

@@ -1,92 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Jackett.Updater</RootNamespace>
<AssemblyName>JackettUpdater</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<RuntimeIdentifier>win</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net452;net461;netcoreapp2.1</TargetFrameworks>
<ApplicationIcon>jackett.ico</ApplicationIcon>
<AssemblyName>JackettUpdater</AssemblyName>
<OutputType>Exe</OutputType>
<Version>0.0.0</Version>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Net.Http.WebRequest" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Web" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UpdaterConsoleOptions.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jackett.Common\Jackett.Common.csproj">
<Project>{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}</Project>
<Name>Jackett.Common</Name>
</ProjectReference>
<ProjectReference Include="..\Jackett.Service\Jackett.Service.csproj">
<Project>{bf611f7b-4658-4cb8-aa9e-0736fadaa3ba}</Project>
<Name>Jackett.Service</Name>
</ProjectReference>
<ProjectReference Include="..\Jackett\Jackett.csproj">
<Project>{e636d5f8-68b4-4903-b4ed-ccfd9c9e899f}</Project>
<Name>Jackett</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="jackett.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,34 +1,59 @@
using CommandLine;
using CommandLine.Text;
using Jackett.Common.Models.Config;
using Jackett.Services;
using Jackett.Common.Services;
using Jackett.Common.Services.Interfaces;
using Jackett.Common.Utils;
using NLog;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Web;
using Jackett.Common;
namespace Jackett.Updater
{
class Program
public class Program
{
static void Main(string[] args)
private IProcessService processService;
private IServiceConfigService windowsService;
private Logger logger;
public static void Main(string[] args)
{
new Program().Run(args);
}
private void Run(string[] args)
{
Engine.BuildContainer(new RuntimeSettings()
RuntimeSettings runtimeSettings = new RuntimeSettings()
{
CustomLogFileName = "updater.txt"
});
Engine.Logger.Info("Jackett Updater v" + GetCurrentVersion());
Engine.Logger.Info("Options \"" + string.Join("\" \"", args) + "\"");
try {
var optionsResult = Parser.Default.ParseArguments<UpdaterConsoleOptions>(args);
};
LogManager.Configuration = LoggingSetup.GetLoggingConfiguration(runtimeSettings);
logger = LogManager.GetCurrentClassLogger();
logger.Info("Jackett Updater v" + GetCurrentVersion());
logger.Info("Options \"" + string.Join("\" \"", args) + "\"");
bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
if (isWindows)
{
//The updater starts before Jackett closes
logger.Info("Pausing for 3 seconds to give Jackett & tray time to shutdown");
System.Threading.Thread.Sleep(3000);
}
processService = new ProcessService(logger);
windowsService = new WindowsServiceConfigService(processService, logger);
var commandLineParser = new Parser(settings => settings.CaseSensitive = false);
try
{
var optionsResult = commandLineParser.ParseArguments<UpdaterConsoleOptions>(args);
optionsResult.WithParsed(options =>
{
ProcessUpdate(options);
@@ -36,14 +61,14 @@ namespace Jackett.Updater
);
optionsResult.WithNotParsed(errors =>
{
Engine.Logger.Error(HelpText.AutoBuild(optionsResult));
Engine.Logger.Error("Failed to process update arguments!");
logger.Error(HelpText.AutoBuild(optionsResult));
logger.Error("Failed to process update arguments!");
Console.ReadKey();
});
}
catch (Exception e)
{
Engine.Logger.Error(e, "Exception applying update!");
logger.Error(e, "Exception applying update!");
}
}
@@ -61,21 +86,20 @@ namespace Jackett.Updater
try
{
var proc = Process.GetProcessById(pid);
Engine.Logger.Info("Killing process " + proc.Id);
logger.Info("Killing process " + proc.Id);
proc.Kill();
var exited = proc.WaitForExit(5000);
if (!exited)
Engine.Logger.Info("Process " + pid.ToString() + " didn't exit within 5 seconds");
logger.Info("Process " + pid.ToString() + " didn't exit within 5 seconds");
}
catch (ArgumentException)
{
Engine.Logger.Info("Process " + pid.ToString() + " is already dead");
logger.Info("Process " + pid.ToString() + " is already dead");
}
catch (Exception e)
{
Engine.Logger.Info("Error killing process " + pid.ToString());
Engine.Logger.Info(e);
logger.Info("Error killing process " + pid.ToString());
logger.Info(e);
}
}
}
@@ -83,7 +107,7 @@ namespace Jackett.Updater
private void ProcessUpdate(UpdaterConsoleOptions options)
{
var updateLocation = GetUpdateLocation();
if(!(updateLocation.EndsWith("\\") || updateLocation.EndsWith("/")))
if (!(updateLocation.EndsWith("\\") || updateLocation.EndsWith("/")))
{
updateLocation += Path.DirectorySeparatorChar;
}
@@ -95,18 +119,18 @@ namespace Jackett.Updater
pids = Array.ConvertAll(pidsStr, pid => int.Parse(pid));
}
var isWindows = System.Environment.OSVersion.Platform != PlatformID.Unix;
var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT;
var trayRunning = false;
var trayProcesses = Process.GetProcessesByName("JackettTray");
if (isWindows)
{
if (trayProcesses.Count() > 0)
{
{
foreach (var proc in trayProcesses)
{
try
{
Engine.Logger.Info("Killing tray process " + proc.Id);
logger.Info("Killing tray process " + proc.Id);
proc.Kill();
trayRunning = true;
}
@@ -118,9 +142,9 @@ namespace Jackett.Updater
// On unix we kill the PIDs after the update so e.g. systemd can automatically restart the process
KillPids(pids);
}
Engine.Logger.Info("Finding files in: " + updateLocation);
logger.Info("Finding files in: " + updateLocation);
var files = Directory.GetFiles(updateLocation, "*.*", SearchOption.AllDirectories);
foreach(var file in files)
foreach (var file in files)
{
var fileName = Path.GetFileName(file).ToLowerInvariant();
@@ -130,20 +154,21 @@ namespace Jackett.Updater
{
continue;
}
try {
Engine.Logger.Info("Copying " + fileName);
try
{
logger.Info("Copying " + fileName);
var dest = Path.Combine(options.Path, file.Substring(updateLocation.Length));
var destDir = Path.GetDirectoryName(dest);
if (!Directory.Exists(destDir))
{
Engine.Logger.Info("Creating directory " + destDir);
logger.Info("Creating directory " + destDir);
Directory.CreateDirectory(destDir);
}
File.Copy(file, dest, true);
}
catch(Exception e)
catch (Exception e)
{
Engine.Logger.Error(e);
logger.Error(e);
}
}
@@ -157,17 +182,16 @@ namespace Jackett.Updater
var deleteDir = Path.Combine(options.Path, oldDir);
if (Directory.Exists(deleteDir))
{
Engine.Logger.Info("Deleting directory " + deleteDir);
logger.Info("Deleting directory " + deleteDir);
Directory.Delete(deleteDir, true);
}
}
catch (Exception e)
{
Engine.Logger.Error(e);
logger.Error(e);
}
}
// delete old files
string[] oldFiles = new string[] {
"Content/css/jquery.dataTables.css",
@@ -209,20 +233,20 @@ namespace Jackett.Updater
"Definitions/torrentwtf.yml",
};
foreach (var oldFIle in oldFiles)
foreach (var oldFile in oldFiles)
{
try
{
var deleteFile = Path.Combine(options.Path, oldFIle);
var deleteFile = Path.Combine(options.Path, oldFile);
if (File.Exists(deleteFile))
{
Engine.Logger.Info("Deleting file " + deleteFile);
logger.Info("Deleting file " + deleteFile);
File.Delete(deleteFile);
}
}
catch (Exception e)
{
Engine.Logger.Error(e);
logger.Error(e);
}
}
@@ -232,26 +256,48 @@ namespace Jackett.Updater
if (options.NoRestart == false)
{
if (trayRunning)
if (isWindows && (trayRunning || options.StartTray) && !string.Equals(options.Type, "WindowsService", StringComparison.OrdinalIgnoreCase))
{
var startInfo = new ProcessStartInfo()
{
Arguments = options.Args,
Arguments = $"--UpdatedVersion \" {EnvironmentUtil.JackettVersion}\"",
FileName = Path.Combine(options.Path, "JackettTray.exe"),
UseShellExecute = true
};
logger.Info("Starting Tray: " + startInfo.FileName + " " + startInfo.Arguments);
Process.Start(startInfo);
if (!windowsService.ServiceExists())
{
//User was running the tray icon, but not using the Windows service, starting Tray icon will start JackettConsole as well
return;
}
}
if(string.Equals(options.Type, "JackettService.exe", StringComparison.InvariantCultureIgnoreCase))
if (string.Equals(options.Type, "WindowsService", StringComparison.OrdinalIgnoreCase))
{
var serviceHelper = new ServiceConfigService(null, null);
if (serviceHelper.ServiceExists())
logger.Info("Starting Windows service");
if (ServerUtil.IsUserAdministrator())
{
serviceHelper.Start();
windowsService.Start();
}
} else
else
{
try
{
var consolePath = Path.Combine(options.Path, "JackettConsole.exe");
processService.StartProcessAndLog(consolePath, "--Start", true);
}
catch
{
logger.Error("Failed to get admin rights to start the service.");
}
}
}
else
{
var startInfo = new ProcessStartInfo()
{
@@ -260,13 +306,21 @@ namespace Jackett.Updater
UseShellExecute = true
};
if (!isWindows)
if (isWindows)
{
//User didn't initiate the update from Windows service and wasn't running Jackett via the tray, must have started from the console
startInfo.Arguments = $"/K {startInfo.FileName} {startInfo.Arguments}";
startInfo.FileName = "cmd.exe";
startInfo.CreateNoWindow = false;
startInfo.WindowStyle = ProcessWindowStyle.Normal;
}
else
{
startInfo.Arguments = startInfo.FileName + " " + startInfo.Arguments;
startInfo.FileName = "mono";
}
Engine.Logger.Info("Starting Jackett: " + startInfo.FileName + " " + startInfo.Arguments);
logger.Info("Starting Jackett: " + startInfo.FileName + " " + startInfo.Arguments);
Process.Start(startInfo);
}
}
@@ -275,7 +329,7 @@ namespace Jackett.Updater
private string GetUpdateLocation()
{
var location = new Uri(Assembly.GetEntryAssembly().GetName().CodeBase);
return new FileInfo(HttpUtility.UrlDecode(location.AbsolutePath)).DirectoryName;
return new FileInfo(WebUtility.UrlDecode(location.AbsolutePath)).DirectoryName;
}
}
}

View File

@@ -1,35 +0,0 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Jackett.Updater")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Jackett.Updater")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("a61e311a-6f8b-4497-b5e4-2ea8994c7bd8")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.0.0.0")]
[assembly: AssemblyFileVersion("0.0.0.0")]

View File

@@ -18,5 +18,8 @@ namespace Jackett.Updater
[Option("KillPids", HelpText = "PIDs which will be killed before (Windows) or after (Unix) the update")]
public string KillPids { get; set; }
[Option("StartTray", HelpText = "Indicates that the updater should start the tray icon")]
public bool StartTray { get; set; }
}
}

View File

@@ -21,7 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Service", "Jackett.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Tray", "Jackett.Tray\Jackett.Tray.csproj", "{FF9025B1-EC14-4AA9-8081-9F69C5E35B63}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Updater", "Jackett.Updater\Jackett.Updater.csproj", "{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Updater", "Jackett.Updater\Jackett.Updater.csproj", "{A61E311A-6F8B-4497-B5E4-2EA8994C7BD8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Test", "Jackett.Test\Jackett.Test.csproj", "{FA22C904-9F5D-4D3C-9122-3E33652E7373}"
EndProject
@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET Core", ".NET Core", "{
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{AA50F785-12B8-4669-8D4F-EAFB49258E60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Server", "Jackett.Server\Jackett.Server.csproj", "{84182782-EDBC-4342-ADA6-72B7694D0862}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -81,6 +83,10 @@ Global
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73}.Release|Any CPU.Build.0 = Release|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -98,6 +104,7 @@ Global
{6B854A1B-9A90-49C0-BC37-9A35C75BCA73} = {2FA9B879-5882-4B39-8D34-9EBCB82B4F2B}
{FF8B9A1B-AE7E-4F14-9C37-DA861D034738} = {AA50F785-12B8-4669-8D4F-EAFB49258E60}
{6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9} = {AA50F785-12B8-4669-8D4F-EAFB49258E60}
{84182782-EDBC-4342-ADA6-72B7694D0862} = {6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {54BC4102-8B85-49C1-BA12-257D941D1B97}

View File

@@ -440,7 +440,7 @@ namespace Jackett.Controllers
torrent_id = release.Guid.ToString(),
details_url = release.Comments.ToString(),
download_url = (release.Link != null ? release.Link.ToString() : release.MagnetUri.ToString()),
imdb_id = release.Imdb.HasValue ? "tt" + release.Imdb : null,
imdb_id = release.Imdb.HasValue ? ParseUtil.GetFullImdbID("tt" + release.Imdb) : null,
freeleech = (release.DownloadVolumeFactor == 0 ? true : false),
type = "movie",
size = (long)release.Size / (1024 * 1024), // This is in MB

View File

@@ -91,6 +91,7 @@ namespace Jackett.Controllers
}
string omdbApiKey = config.omdbkey;
string omdbApiUrl = config.omdburl;
serverConfig.UpdateDisabled = updateDisabled;
serverConfig.UpdatePrerelease = preRelease;
@@ -101,9 +102,10 @@ namespace Jackett.Controllers
Engine.SetLogLevel(logging ? LogLevel.Debug : LogLevel.Info);
serverConfig.RuntimeSettings.TracingEnabled = logging;
if (omdbApiKey != serverConfig.OmdbApiKey)
if (omdbApiKey != serverConfig.OmdbApiKey || omdbApiUrl != serverConfig.OmdbApiUrl)
{
serverConfig.OmdbApiKey = omdbApiKey;
serverConfig.OmdbApiUrl = omdbApiUrl.TrimEnd('/');
configService.SaveConfig(serverConfig);
// HACK
indexerService.InitAggregateIndexer();