Compare commits

..

129 Commits

Author SHA1 Message Date
Bogdan
1fd188fe7a Add wiki fragment and translation to UpdateCheck 2023-05-06 19:06:43 +03:00
Lars
5e5699fbbe New: Option to use Telegram topics for notifications
(cherry picked from commit c8933d812490ba375c6f7e07cd4355921dc513ac)

Closes #1640
2023-05-06 16:33:49 +03:00
Mark McDowall
d61275e6db New: Improve path validation when handling paths from different OSes
(cherry picked from commit 0321368cc392d7a0a488409bf6bd586ba45497af)
2023-05-06 16:30:20 +03:00
Benjamin Staneck
dca3e939f0 Update webpack and webpack-cli
(cherry picked from commit 8bcaa17e25161aeedd9ca8488a4bb6f4c423de2b)
2023-05-06 11:00:01 +03:00
Benjamin Staneck
26ac66c0e1 Fixed some aria violations
(cherry picked from commit 7aa846343815105e3576e6aa20eac64fcb0edf8d)
2023-05-06 10:50:05 +03:00
Benjamin Staneck
649b301444 New: Updated button and calendar outline colors for dark theme
(cherry picked from commit 5d873fafec07c9d67ad8d2c16eecea195a78eecf)
2023-05-06 10:47:58 +03:00
Benjamin Staneck
78ed2a1af0 Update core-js and use defaults for browserlist
(cherry picked from commit de4cfefde4d00aba829356541b02e8f9a7729977)
2023-05-06 10:44:51 +03:00
Benjamin Staneck
a4854b7b5f Use minified jquery
(cherry picked from commit bb77bb640c0529ca3e6386ec657e64ebafad02f4)
2023-05-06 10:41:55 +03:00
Weblate
97edf495bd Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (508 of 508 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-05-05 15:26:39 +03:00
Mark McDowall
d10bdf4676 New style scrollbar in Firefox
(cherry picked from commit 9bd783d49c91600d6575fc86e7bdd56858c213f1)
2023-05-05 04:21:09 +03:00
Bogdan
03647143e3 Remove unused ReactDOM import 2023-05-05 04:14:51 +03:00
Mark McDowall
8090dc9983 Fixed: File browser
(cherry picked from commit f7ce5c7b115ea0d12ab63f19960c473e09e30f3d)
2023-05-05 04:04:58 +03:00
Qstick
5bc1f345c0 Auto-reply for Log Label
(cherry picked from commit d851ecdf2f826f25b9d2d67d3b7e9e3642bc5299)
2023-05-04 20:24:26 +03:00
Weblate
4ef01f5640 Translated using Weblate (Chinese (Simplified) (zh_CN))
Currently translated at 96.6% (489 of 506 strings)

Translated using Weblate (French)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Remy <remy@mrbk.fr>
Co-authored-by: hellojuly <pangcheinug@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-05-03 22:47:30 +03:00
Bogdan
f13d5c5a14 Fix typo in ShowSearchHelpText 2023-05-03 21:24:22 +03:00
Bogdan
dc8773cf79 Update and sort translations 2023-05-03 20:25:04 +03:00
Weblate
cad774e250 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (506 of 506 strings)

Translated using Weblate (Dutch)

Currently translated at 83.0% (420 of 506 strings)

Translated using Weblate (French)

Currently translated at 100.0% (506 of 506 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: R00T99 <ddaa5e30-3d20-41f9-8ed8-bba67e8acf80@anonaddy.me>
Co-authored-by: foXaCe <foxace66@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-05-02 13:05:17 +03:00
Bogdan
b28eee578a Log apps sync not having intersecting tags as debug 2023-05-02 10:23:13 +03:00
Bogdan
5b8c7d0b79 New: (Rarbg) Add caching results 2023-05-02 08:08:51 +03:00
Bogdan
8bdc7a6db7 Check response status code in classes that extend RssParser 2023-05-02 08:06:21 +03:00
Tebowy Seba
cb189b8f61 Fixed: (BrokenStones) Restored, site moved domains 2023-05-02 01:47:17 +03:00
Bogdan
24468db376 Fixed: (AudioBookBay) New indexer url 2023-05-01 00:52:12 +03:00
Bakerboy448
9b10cea556 Fixed: Improve Rarbg Rate Limit Messaging 2023-04-30 20:03:56 +03:00
Weblate
d8fb71d501 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-04-29 23:31:37 +03:00
Bogdan
fc39a11ece (Apprise) Change BaseUrl to ServerUrl 2023-04-29 23:30:49 +03:00
Bakerboy448
40dc4de47d Fixed: Missing Translates 2023-04-29 15:29:46 -05:00
Bogdan
a0e2f3324c Fixed: (AnimeBytes) Cache result releases 2023-04-29 19:29:38 +03:00
Qstick
1bcc3b426e Bump version to 1.5.0 2023-04-29 11:27:02 -05:00
Bogdan
66f5fd2a26 Fixed: (BroadcastheNet) Add tests & prefer TvdbId 2023-04-29 19:00:26 +03:00
Bogdan
b5e5701791 Trim search term 2023-04-29 19:00:26 +03:00
Bakerboy448
1a9b202afe Fixed: Clarify App Sync-Indexer Logging for tags 2023-04-29 09:12:31 -05:00
Qstick
309f42bac5 Fixed: Normalize ImdbId for incoming requests
Fixes #1631
2023-04-28 22:38:19 -05:00
Weblate
ed330ea657 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (484 of 484 strings)

Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-04-26 07:11:28 +03:00
Bogdan
fc6a31ea78 Fixed: (AnimeBytes) Parsing season improvements 2023-04-25 15:20:55 +03:00
Bogdan
25ba9195cf Fixed: (XSpeeds) Update category mappings 2023-04-25 15:18:35 +03:00
Bogdan
681f06e321 Fixed: Disable checking for updates when in debug runtime
Same as a8e2b1520a

Closes #1626
2023-04-24 17:08:02 +03:00
Bogdan
af7fb442d2 New: Add PixelHD 2023-04-24 14:38:34 +03:00
Bogdan
2061b9142f Fixed: (XSpeeds) Update category mappings 2023-04-24 06:10:15 +03:00
Bogdan
b97f6f8ddf Fixed: (UI) Add New Indexer button to open the Add Indexer modal
Fixes #1625
Closes #1389
2023-04-24 04:31:11 +03:00
Qstick
f31c0bb1de Bump version to 1.4.1 2023-04-23 13:18:30 -05:00
Qstick
65e6aa05c3 Bump version to 1.4.0 2023-04-23 12:15:23 -05:00
Bogdan
fb20b3e61b Fixed: (AnimeBytes) Add tests for season parsing 2023-04-23 14:55:50 +03:00
Bogdan
b8a77830aa Fixed: (AnimeBytes) Parse response with STJson 2023-04-23 07:44:46 +03:00
Bogdan
d2ba52cdce Fixed: (Indexers) Hide errors with SuppressHttpErrorStatusCodes 2023-04-23 07:44:16 +03:00
Bogdan
43f881c442 Fixed: (Nebulance) Don't parse invalid response as JSON 2023-04-23 06:40:26 +03:00
Bogdan
4a5e923999 Fixed: (AnimeBytes) Parse season only for category Anime 2023-04-22 20:37:09 +03:00
Bogdan
57e1b6b4a0 Fixed: (AnimeBytes) Improve season/episode detection 2023-04-22 20:19:44 +03:00
Bogdan
9cc60760c3 Fixed: (AnimeBytes) Exclude RAW only for category Anime 2023-04-22 20:17:36 +03:00
Weblate
2811feb14e Translated using Weblate (Chinese (Simplified) (zh_CN))
Currently translated at 99.7% (483 of 484 strings)

Translated using Weblate (Portuguese)

Currently translated at 78.3% (379 of 484 strings)

Co-authored-by: PedroBuffon <henriquebuffon@gmail.com>
Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-04-22 14:21:43 +03:00
Bogdan
46af9223bc Fixed: (BakaBT) Update check if logged in
Resolves #1617
2023-04-22 07:47:14 +03:00
Bogdan
025156978b Fixed: (AnimeBytes) Ignore useless extensions to improve single file names 2023-04-22 03:13:56 +03:00
Bogdan
d3ca861aea Fixed: (AnimeBytes) Remove The Movie from search term 2023-04-21 07:36:52 +03:00
Bogdan
c9249ed583 Fixed: (UI) Typo in hover border colors 2023-04-20 11:51:44 +03:00
Bogdan
94cc56d0f6 Fixed: (AnimeBytes) Improve season/episode detection 2023-04-20 10:39:15 +03:00
Bogdan
c8addc0d62 Rearrange params in Apprise 2023-04-20 08:10:05 +03:00
Bogdan
2015156061 New: (Apprise) Add notification type
Closes #1619
2023-04-20 07:45:51 +03:00
Bogdan
742c680014 Fixed: (AnimeBytes) Add Remux to release titles when possible 2023-04-20 03:08:13 +03:00
Bogdan
b1add3f649 Fixed: (AnimeBytes) Parse M2TS property as BR-DISK 2023-04-20 01:54:18 +03:00
bakerboy448
65d6d518d7 New: Improve applications error reporting for requests (#1618)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2023-04-19 03:44:22 +03:00
Cedric Lewe
bc8ba5ca02 Fixed: (BakaBT) Update login check (#1617)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2023-04-18 22:43:17 +03:00
Bogdan
6aebc4ee01 Revert properties order in SonarrSettings 2023-04-18 04:47:09 +03:00
Bogdan
9bbe51253b New: Add default urls to applications 2023-04-18 04:42:08 +03:00
Mark McDowall
88fbc30be2 New: Improved messaging when qBittorrent fails due to host header rejection
(cherry picked from commit 48b4cc5f3ffa0cb8eea6748db9091267216cef4f)
2023-04-18 03:42:48 +03:00
bakerboy448
5fdc6ee25d Fixed: (BroadcasTheNet) Improve season/episode/daily episode searches 2023-04-17 23:44:37 +03:00
Bogdan
4eb5a2d613 Fixed: (Cardigann) Simplify creating CardigannRequest 2023-04-17 21:42:05 +03:00
Bogdan
122883053a Fixed: (Cardigann) Respect Followredirect for login forms
Fixes #526
2023-04-17 19:38:37 +03:00
Bogdan
28d09cd384 Fixed: (Rarbg) Simplify retry fetching for expired tokens 2023-04-17 06:21:07 +03:00
Bogdan
17be8bb68a Add SuppressHttpErrorStatusCodes to HttpRequestBuilder 2023-04-17 06:21:07 +03:00
Qstick
c5baded3d6 Simplify DatabaseType logic 2023-04-16 20:09:04 -05:00
Bogdan
349cfacdca Rename CC to Cc 2023-04-17 04:06:29 +03:00
Bogdan
788fa6d96a Fixed: (Database) Improve Version detection 2023-04-17 04:04:01 +03:00
Bogdan
fbea5bbc06 Fixed: (CookieUtil) Add tests 2023-04-16 05:13:30 +03:00
Bogdan
d667c7d853 Fixed: Use Array.Empty and fix a few multiple enumerations
(cherry picked from commit 11d91faaada0e70910c832ce405ddeed52a24172)
2023-04-16 05:09:12 +03:00
Qstick
a9e1204a9b Fixed: Validate if equals or child for startup folder
(cherry picked from commit 0991cfe27efd6ddb533227b25754661e18d7e9ad)
2023-04-16 05:07:50 +03:00
Bogdan
88e3f86262 Fixed: Migrate to FluentValidation 9 2023-04-16 05:07:50 +03:00
Bogdan
1c173fc984 Fixed: (Cardigann) Update namespace and use nameof() 2023-04-15 07:04:43 +03:00
Bogdan
6e8f3d814a Fixed: (Cardigann) Log requests as debug 2023-04-15 06:54:44 +03:00
Bogdan
14e105e37e Fixed: (Redacted) Add tests 2023-04-14 23:50:33 +03:00
Bogdan
9e0deb8f74 Fixed: (TorrentInfo) Cleanup redundant Freeleech property 2023-04-14 23:13:21 +03:00
Bogdan
245e573089 Fixed: (TorrentPotato) Update namespace 2023-04-14 23:11:03 +03:00
Bogdan
5e8bfa2ffb Fixed: (RuTracker) Add new indexer url
Fixes #1610
2023-04-14 09:06:41 +03:00
Bogdan
555c924e50 New: Add version and timestamp to backup archive
(cherry picked from commit ed3d880974ae6a1430866eebaf72533f35258f6f)

Fixes #662
Closes #1600
2023-04-14 06:06:15 +03:00
Bogdan
8404b85624 Fixed: (AnimeBytes) API responds now with size as integer 2023-04-14 05:58:23 +03:00
Bogdan
dc5e6d29e1 Bump dotnet to 6.0.16 2023-04-14 03:21:01 +03:00
Bogdan
8c42b7a69b Update DryIoc, Newtonsoft.Json, Sentry, SharpZipLib, MailKit 2023-04-14 01:56:21 +03:00
Weblate
3a6ebdef8a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (484 of 484 strings)

Translated using Weblate (Spanish)

Currently translated at 77.0% (373 of 484 strings)

Translated using Weblate (German)

Currently translated at 95.8% (464 of 484 strings)

Co-authored-by: Deflector8249 <lh2jwko5@gomail.me>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: xuko <jorge.xuko@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-04-13 01:29:23 +03:00
Bogdan
5f57957462 Fixed: (AnimeBytes) Improve release group detection 2023-04-13 01:13:03 +03:00
Bogdan
12526c1bb3 Fixed: (AnimeBytes) Improve season detection 2023-04-12 23:01:24 +03:00
Bogdan
29f049f766 Fixed: (AnimeBytes) Fix tests for release title structure change 2023-04-12 04:19:37 +03:00
Bogdan
40f4e1b82a Fixed: (AnimeBytes) Change release title structure for movies 2023-04-12 03:35:28 +03:00
Bogdan
065fbb30bf Fixed: Support the old broken functionality in GetValueEnum
(cherry picked from commit 2c4e1be12ad5e3b8362f83b8185c143f8e66062b)

Fixes #1602
2023-04-11 21:42:20 +03:00
Bogdan
ea24a81ef7 Fixed: (API) Log errors in Newznab response 2023-04-11 18:32:45 +03:00
Bogdan
451f60319f Fixed: (Cardigann) Add check for request.inputs, since are null when pathselector is used
Fixes #1158
2023-04-11 18:31:11 +03:00
Bogdan
c6ed5d65e0 Fixed: (Core) Ensure default config file on starting app
Fixes #674
Fixes #1588
2023-04-11 17:22:48 +03:00
bakerboy448
4e5cd05bbd Fixed: Improve Indexer Tags Help Text 2023-04-10 22:47:27 +03:00
Bogdan
6b2b953686 Fixed: (Cardigann) Catch errors when search.rows.count is not present 2023-04-10 07:49:22 +03:00
Mark McDowall
31c05be9de Fixed: Prevent getting disk space from returning no information when it partially fails
(cherry picked from commit 2c65e4fa41418157d0d27b34c3bab80158cff219)
2023-04-09 22:16:51 -05:00
Mark McDowall
bc852c0b55 Fixed: USB drives mounted to folders are treated as different mounts
(cherry picked from commit 75378f7bde90b9d3d9b72404c25c017da2cd147c)
2023-04-09 22:15:51 -05:00
Qstick
18651d8be1 Cleanup old Radarr parsing library conditional 2023-04-09 22:14:53 -05:00
Qstick
1608095345 Fixed: Cleanup TaskManager, add BackupInterval limits 2023-04-09 22:11:46 -05:00
Bogdan
7700014ceb Fixed: (PassThePopcorn) Disable grouping, add pagination and use STJson 2023-04-10 05:32:33 +03:00
Bogdan
3fbc2912f0 Fixed: (AnimeBytes) Add limit and refactor parser 2023-04-10 05:03:43 +03:00
Mark McDowall
3192990874 Fixed: Number input changing while scrolling
(cherry picked from commit cc46ed56b4b70fe1f1443c0a927383f19c989c47)
2023-04-09 20:19:02 -05:00
Bogdan
fb908e8e19 Fixed: Use project name as relative path 2023-04-10 04:15:02 +03:00
Bogdan
8e60c707b2 Fixed: (Cardigann) Skip rows parsing on zero rows 2023-04-10 02:55:32 +03:00
Bogdan
a184bb0784 Fixed: (Core) Use MinBy and MaxBy 2023-04-09 21:14:40 +03:00
bakerboy448
e5ccbaaf24 Update bug_report.yml - trace logs checkbox 2023-04-09 20:25:11 +03:00
Weblate
362e0acad1 Translated using Weblate (Czech)
Currently translated at 70.0% (339 of 484 strings)

Co-authored-by: tomas15420 <tomas15420@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translation: Servarr/Prowlarr
2023-04-09 20:23:39 +03:00
Bogdan
54d06460d0 Fixed: (Avistaz/SecretCinema) Fix tests 2023-04-09 16:49:56 +03:00
Bogdan
c11bcf4c41 Fixed: (SecretCinema) Fix PublishDate timezone 2023-04-09 16:18:05 +03:00
Bogdan
2e58583263 Fixed: (Avistaz) Fix PublishDate timezone 2023-04-09 16:14:09 +03:00
Bogdan
bf7f769f13 Fixed: (AvistaZ) Don't log http errors like 404 for imdb/tmdb/tvdb searches 2023-04-08 22:55:05 +03:00
Bogdan
7820a83a5d Fixed: (Indexers) Include exception message in ValidationFailure 2023-04-08 22:49:53 +03:00
Bogdan
d937bdac69 Fixed: (NZBIndex) Request generator cleanup 2023-04-08 22:17:29 +03:00
Qstick
ebca32af46 Fixed: (NzbIndex) Paging starts at 0
Fixes #1586
2023-04-08 09:52:17 -05:00
Bogdan
21bda07510 Fixed: (Toloka) Add authors.gif to check for FL
Co-authored-by: odayny <odayny@users.noreply.github.com>
2023-04-08 01:11:37 +03:00
Bogdan
f638cf34d1 Fixed: (UI) Fix search sorting by empty categories 2023-04-08 00:52:30 +03:00
Bogdan
b7fcdb5356 Fixed: (AnimeBytes) Add search by year 2023-04-06 17:23:54 +03:00
bakerboy448
2e4fa9d06d Fixed: (AnimeBytes) Change RateLimit to 4s (#1580) 2023-04-06 04:32:58 +03:00
bakerboy448
9b50fc40ca Fix: (MaM) Improve No results logic (#1578) 2023-04-05 22:21:25 +03:00
bakerboy448
3c60159df0 Fixed: (AnimeBytes) RateLimit 1req per 10s
Fixes #1572
2023-04-05 20:53:04 +03:00
Bogdan
e075003c8b Fixed: (FileList) Change TZ to account DST 2023-04-05 06:28:07 +03:00
Servarr
b19202d9f5 Automated API Docs update 2023-04-05 05:45:34 +03:00
Bogdan
2784ee8ce6 Fixed: (UI) Update frontend packages 2023-04-05 05:14:08 +03:00
Bogdan
5aa4a5faaa Fixed: (Tags) Show applications in tag details 2023-04-05 05:12:56 +03:00
Weblate
1d00b40f90 Translated using Weblate (Portuguese)
Currently translated at 77.6% (376 of 484 strings)

Translated using Weblate (Hebrew)

Currently translated at 80.3% (389 of 484 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN))

Currently translated at 100.0% (484 of 484 strings)

Co-authored-by: Cassio Rizzi <clrizzi@gmail.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 雨 <625250353@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-04-03 19:54:21 -05:00
Qstick
93dd378ade Bump version to 1.3.3 2023-04-02 15:14:06 -05:00
Bogdan
534ca73bf8 Fixed: (Toloka) Add FreeleechOnly setting 2023-04-01 21:12:23 +03:00
Bogdan
bceebc34c1 New: (Cardigann) Bump to v9 (#1551)
* New: (Cardigann) Add MissingAttributeEqualsNoResults support

(cherry picked from commit 4e8bb37a5c)

* New: (Cardigann) Add AllowEmptyInputs

* New: (Cardigann) Bump to v9

* New: (Cardigann) Add default value for fields
2023-03-30 14:57:04 +03:00
224 changed files with 6368 additions and 2453 deletions

View File

@@ -71,3 +71,10 @@ body:
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true
- type: checkboxes
attributes:
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
description: Trace logs are generally required for all bug reports
options:
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue.
required: true

View File

@@ -13,4 +13,11 @@
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a indexer request. Please use our Indexer request [site](https://requests.prowlarr.com/)
close: true
close: true
'Status: Logs Needed':
comment: >
:wave: @{issue-author}, in order to help you further we'll need to see logs.
You'll need to enable trace logging and replicate the problem that you encountered.
Guidance on how to enable trace logging can be found in
our [troubleshooting guide](https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files).

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.3.2'
majorVersion: '1.5.0'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.405'
dotnetVersion: '6.0.408'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'

View File

@@ -50,7 +50,7 @@ module.exports = (env) => {
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
jquery: 'jquery/dist/jquery.min'
},
fallback: {
buffer: false,

View File

@@ -17,7 +17,7 @@ class DescriptionListItem extends Component {
} = this.props;
return (
<span>
<div>
<DescriptionListItemTitle
className={titleClassName}
>
@@ -29,7 +29,7 @@ class DescriptionListItem extends Component {
>
{data}
</DescriptionListItemDescription>
</span>
</div>
);
}
}

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Alert from 'Components/Alert';
import PathInput from 'Components/Form/PathInput';
import Button from 'Components/Link/Button';
@@ -39,7 +38,7 @@ class FileBrowserModalContent extends Component {
constructor(props, context) {
super(props, context);
this._scrollerNode = null;
this._scrollerRef = React.createRef();
this.state = {
isFileBrowserModalOpen: false,
@@ -57,21 +56,10 @@ class FileBrowserModalContent extends Component {
currentPath !== prevState.currentPath
) {
this.setState({ currentPath });
this._scrollerNode.scrollTop = 0;
this._scrollerRef.current.scrollTop = 0;
}
}
//
// Control
setScrollerRef = (ref) => {
if (ref) {
this._scrollerNode = ReactDOM.findDOMNode(ref);
} else {
this._scrollerNode = null;
}
};
//
// Listeners
@@ -145,7 +133,7 @@ class FileBrowserModalContent extends Component {
/>
<Scroller
ref={this.setScrollerRef}
ref={this._scrollerRef}
className={styles.scroller}
scrollDirection={scrollDirections.BOTH}
>

View File

@@ -112,6 +112,12 @@ class TextInput extends Component {
this._isMouseTarget = false;
};
onWheel = () => {
if (this.props.type === 'number') {
this._input.blur();
}
};
//
// Render
@@ -161,6 +167,7 @@ class TextInput extends Component {
onKeyUp={this.onKeyUp}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
onWheel={this.onWheel}
/>
);
}

View File

@@ -23,6 +23,7 @@ function IconButton(props) {
className,
isDisabled && styles.isDisabled
)}
aria-label="Table Options Button"
isDisabled={isDisabled}
{...otherProps}
>

View File

@@ -56,6 +56,7 @@ class PageHeader extends Component {
<img
className={styles.logo}
src={`${window.Prowlarr.urlBase}/Content/Images/logo.png`}
alt="Prowlarr Logo"
/>
</Link>
</div>
@@ -74,6 +75,7 @@ class PageHeader extends Component {
<IconButton
className={styles.donate}
name={icons.HEART}
aria-label="Donate"
to="https://prowlarr.com/donate"
size={14}
/>

View File

@@ -21,7 +21,7 @@ function PageHeaderActionsMenu(props) {
return (
<div>
<Menu alignMenu={align.RIGHT}>
<MenuButton className={styles.menuButton}>
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon
name={icons.INTERACTIVE}
/>

View File

@@ -56,7 +56,9 @@ function ProgressBar(props) {
styles[kind],
enableColorImpairedMode && 'colorImpaired'
)}
aria-valuenow={progress}
role="meter"
aria-label={`Progress Bar at ${progress.toFixed(0)}%`}
aria-valuenow={progress.toFixed(0)}
aria-valuemin="0"
aria-valuemax="100"
style={{ width: progressPercent }}

View File

@@ -6,6 +6,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
import translate from 'Utilities/String/translate';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryRowParameter from './HistoryRowParameter';
@@ -193,7 +194,7 @@ class HistoryRow extends Component {
{
data.season ?
<HistoryRowParameter
title='Season'
title={translate('Season')}
value={data.season}
/> :
null
@@ -202,7 +203,7 @@ class HistoryRow extends Component {
{
data.episode ?
<HistoryRowParameter
title='Episode'
title={translate('Episode')}
value={data.episode}
/> :
null
@@ -211,7 +212,7 @@ class HistoryRow extends Component {
{
data.artist ?
<HistoryRowParameter
title='Artist'
title={translate('Artist')}
value={data.artist}
/> :
null
@@ -220,7 +221,7 @@ class HistoryRow extends Component {
{
data.album ?
<HistoryRowParameter
title='Album'
title={translate('Album')}
value={data.album}
/> :
null
@@ -229,7 +230,7 @@ class HistoryRow extends Component {
{
data.label ?
<HistoryRowParameter
title='Label'
title={translate('Label')}
value={data.label}
/> :
null
@@ -238,7 +239,7 @@ class HistoryRow extends Component {
{
data.track ?
<HistoryRowParameter
title='Track'
title={translate('Track')}
value={data.track}
/> :
null
@@ -247,7 +248,7 @@ class HistoryRow extends Component {
{
data.year ?
<HistoryRowParameter
title='Year'
title={translate('Year')}
value={data.year}
/> :
null
@@ -256,7 +257,7 @@ class HistoryRow extends Component {
{
data.genre ?
<HistoryRowParameter
title='Genre'
title={translate('Genre')}
value={data.genre}
/> :
null
@@ -265,7 +266,7 @@ class HistoryRow extends Component {
{
data.author ?
<HistoryRowParameter
title='Author'
title={translate('Author')}
value={data.author}
/> :
null
@@ -274,7 +275,7 @@ class HistoryRow extends Component {
{
data.bookTitle ?
<HistoryRowParameter
title='Book'
title={translate('Book')}
value={data.bookTitle}
/> :
null
@@ -283,7 +284,7 @@ class HistoryRow extends Component {
{
data.publisher ?
<HistoryRowParameter
title='Publisher'
title={translate('Publisher')}
value={data.publisher}
/> :
null
@@ -351,6 +352,11 @@ class HistoryRow extends Component {
`${data.elapsedTime}ms` :
null
}
{
data.cached === '1' ?
' (cached)' :
null
}
</TableRowCell>
);
}
@@ -376,14 +382,14 @@ class HistoryRow extends Component {
<IconButton
name={icons.SEARCH}
onPress={this.onSearchPress}
title='Repeat Search'
title={translate('RepeatSearch')}
/> :
null
}
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
title='History Details'
title={translate('HistoryDetails')}
/>
</TableRowCell>
);

View File

@@ -222,7 +222,11 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
<PageToolbarSeparator />
<IndexerIndexSelectModeButton
label={isSelectMode ? 'Stop Selecting' : 'Select Indexer'}
label={
isSelectMode
? translate('StopSelecting')
: translate('SelectIndexer')
}
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
isSelectMode={isSelectMode}
overflowComponent={IndexerIndexSelectModeMenuItem}
@@ -230,7 +234,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
/>
<IndexerIndexSelectAllButton
label="SelectAll"
label={translate('SelectAll')}
isSelectMode={isSelectMode}
overflowComponent={IndexerIndexSelectAllMenuItem}
/>
@@ -245,7 +249,10 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
optionsComponent={IndexerIndexTableOptions}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton label="Options" iconName={icons.TABLE} />
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
@@ -276,7 +283,9 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? <div>Unable to load indexers</div> : null}
{!isFetching && !!error ? (
<div>{translate('UnableToLoadIndexers')}</div>
) : null}
{isLoaded ? (
<div className={styles.contentBodyContainer}>
@@ -295,7 +304,10 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
) : null}
{!error && isPopulated && !items.length ? (
<NoIndexer totalItems={totalItems} />
<NoIndexer
totalItems={totalItems}
onAddIndexerPress={onAddIndexerPress}
/>
) : null}
</PageContentBody>
{isLoaded && !!jumpBarItems.order.length ? (

View File

@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
interface IndexerIndexSelectAllButtonProps {
label: string;
@@ -32,7 +33,7 @@ function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
return isSelectMode ? (
<PageToolbarButton
label={allSelected ? 'Unselect All' : 'Select All'}
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icon}
onPress={onPress}
/>

View File

@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import { SelectActionType, useSelect } from 'App/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
interface IndexerIndexSelectAllMenuItemProps {
label: string;
@@ -33,7 +34,7 @@ function IndexerIndexSelectAllMenuItem(
return isSelectMode ? (
<PageToolbarOverflowMenuItem
label={allSelected ? 'Unselect All' : 'Select All'}
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={iconName}
onPress={onPressWrapper}
/>

View File

@@ -14,6 +14,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import translate from 'Utilities/String/translate';
import styles from './TagsModalContent.css';
interface TagsModalContentProps {
@@ -70,7 +71,7 @@ function TagsModalContent(props: TagsModalContentProps) {
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
@@ -81,7 +82,7 @@ function TagsModalContent(props: TagsModalContentProps) {
</FormGroup>
<FormGroup>
<FormLabel>Apply Tags</FormLabel>
<FormLabel>{translate('ApplyTags')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
@@ -89,17 +90,17 @@ function TagsModalContent(props: TagsModalContentProps) {
value={applyTags}
values={applyTagsOptions}
helpTexts={[
'How to apply tags to the selected series',
'Add: Add the tags the existing list of tags',
'Remove: Remove the entered tags',
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
translate('ApplyTagsHelpTexts1'),
translate('ApplyTagsHelpTexts2'),
translate('ApplyTagsHelpTexts3'),
translate('ApplyTagsHelpTexts4'),
]}
onChange={onApplyTagsChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Result</FormLabel>
<FormLabel>{translate('Result')}</FormLabel>
<div className={styles.result}>
{indexerTags.map((id) => {
@@ -116,7 +117,11 @@ function TagsModalContent(props: TagsModalContentProps) {
return (
<Label
key={tag.id}
title={removeTag ? 'Removing tag' : 'Existing tag'}
title={
removeTag
? translate('RemoveTagRemovingTag')
: translate('RemoveTagExistingTag')
}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
>
@@ -140,7 +145,7 @@ function TagsModalContent(props: TagsModalContentProps) {
return (
<Label
key={tag.id}
title={'Adding tag'}
title={translate('AddingTag')}
kind={kinds.SUCCESS}
size={sizes.LARGE}
>

View File

@@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import selectTableOptions from './selectTableOptions';
interface IndexerIndexTableOptionsProps {
@@ -32,13 +33,13 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
return (
<Fragment>
<FormGroup>
<FormLabel>Show Search</FormLabel>
<FormLabel>{translate('ShowSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText="Show search button on hover"
helpText={translate('ShowSearchHelpText')}
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>

View File

@@ -33,8 +33,8 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
const enableTitle = redirect
? 'Indexer is Enabled, Redirect is Enabled'
: 'Indexer is Enabled';
? translate('EnabledRedirected')
: translate('Enabled');
return (
<Component className={className} {...otherProps}>
@@ -43,7 +43,7 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon : icons.BLOCKLIST}
title={enabled ? enableTitle : 'Indexer is Disabled'}
title={enabled ? enableTitle : translate('EnabledIndexerIsDisabled')}
/>
}
{status ? (

View File

@@ -10,6 +10,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import StatsFilterMenu from './StatsFilterMenu';
import styles from './Stats.css';
@@ -188,53 +189,53 @@ function Stats(props) {
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title='Average Response Times (ms)'
title={translate('AverageResponseTimesMs')}
/>
</div>
<div className={styles.fullWidthChart}>
<BarChart
data={getFailureRateData(item.indexers)}
title='Indexer Failure Rate'
title={translate('IndexerFailureRate')}
kind={kinds.WARNING}
/>
</div>
<div className={styles.halfWidthChart}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title='Total Indexer Queries'
title={translate('TotalIndexerQueries')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title='Total Indexer Successful Grabs'
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title='Total User Agent Queries'
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title='Total User Agent Grabs'
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title='Total Host Queries'
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title='Total Host Grabs'
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>

View File

@@ -17,6 +17,7 @@ function TagDetailsModalContent(props) {
indexers,
notifications,
indexerProxies,
applications,
onModalClose,
onDeleteTagPress
} = props;
@@ -79,6 +80,21 @@ function TagDetailsModalContent(props) {
}
</FieldSet>
}
{
!!applications.length &&
<FieldSet legend={translate('Applications')}>
{
applications.map((item) => {
return (
<div key={item.id}>
{item.name}
</div>
);
})
}
</FieldSet>
}
</ModalBody>
<ModalFooter>
@@ -110,6 +126,7 @@ TagDetailsModalContent.propTypes = {
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerProxies: PropTypes.arrayOf(PropTypes.object).isRequired,
applications: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteTagPress: PropTypes.func.isRequired
};

View File

@@ -18,16 +18,24 @@ function createMatchingIndexersSelector() {
function createMatchingIndexerProxiesSelector() {
return createSelector(
(state, { notificationIds }) => notificationIds,
(state) => state.settings.notifications.items,
(state, { indexerProxyIds }) => indexerProxyIds,
(state) => state.settings.indexerProxies.items,
findMatchingItems
);
}
function createMatchingNotificationsSelector() {
return createSelector(
(state, { indexerProxyIds }) => indexerProxyIds,
(state) => state.settings.indexerProxies.items,
(state, { notificationIds }) => notificationIds,
(state) => state.settings.notifications.items,
findMatchingItems
);
}
function createMatchingApplicationsSelector() {
return createSelector(
(state, { applicationIds }) => applicationIds,
(state) => state.settings.applications.items,
findMatchingItems
);
}
@@ -37,11 +45,13 @@ function createMapStateToProps() {
createMatchingIndexersSelector(),
createMatchingIndexerProxiesSelector(),
createMatchingNotificationsSelector(),
(indexers, indexerProxies, notifications) => {
createMatchingApplicationsSelector(),
(indexers, indexerProxies, notifications, applications) => {
return {
indexers,
indexerProxies,
notifications
notifications,
applications
};
}
);

View File

@@ -55,7 +55,8 @@ class Tag extends Component {
label,
notificationIds,
indexerIds,
indexerProxyIds
indexerProxyIds,
applicationIds
} = this.props;
const {
@@ -66,7 +67,8 @@ class Tag extends Component {
const isTagUsed = !!(
indexerIds.length ||
notificationIds.length ||
indexerProxyIds.length
indexerProxyIds.length ||
applicationIds.length
);
return (
@@ -102,6 +104,13 @@ class Tag extends Component {
{indexerProxyIds.length} {indexerProxyIds.length > 1 ? translate('IndexerProxies') : translate('IndexerProxy')}
</div>
}
{
!!applicationIds.length &&
<div>
{applicationIds.length} {applicationIds.length > 1 ? translate('Applications') : translate('Application')}
</div>
}
</div>
}
@@ -118,6 +127,7 @@ class Tag extends Component {
indexerIds={indexerIds}
notificationIds={notificationIds}
indexerProxyIds={indexerProxyIds}
applicationIds={applicationIds}
isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose}
onDeleteTagPress={this.onDeleteTagPress}
@@ -143,13 +153,15 @@ Tag.propTypes = {
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerProxyIds: PropTypes.arrayOf(PropTypes.number).isRequired,
applicationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onConfirmDeleteTag: PropTypes.func.isRequired
};
Tag.defaultProps = {
indexerIds: [],
notificationIds: [],
indexerProxyIds: []
indexerProxyIds: [],
applicationIds: []
};
export default Tag;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchApplications, fetchIndexerProxies, fetchNotifications } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags';
@@ -27,7 +27,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchNotifications: fetchNotifications,
dispatchFetchIndexerProxies: fetchIndexerProxies
dispatchFetchIndexerProxies: fetchIndexerProxies,
dispatchFetchApplications: fetchApplications
};
class MetadatasConnector extends Component {
@@ -39,12 +40,14 @@ class MetadatasConnector extends Component {
const {
dispatchFetchTagDetails,
dispatchFetchNotifications,
dispatchFetchIndexerProxies
dispatchFetchIndexerProxies,
dispatchFetchApplications
} = this.props;
dispatchFetchTagDetails();
dispatchFetchNotifications();
dispatchFetchIndexerProxies();
dispatchFetchApplications();
}
//
@@ -62,7 +65,8 @@ class MetadatasConnector extends Component {
MetadatasConnector.propTypes = {
dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchIndexerProxies: PropTypes.func.isRequired
dispatchFetchIndexerProxies: PropTypes.func.isRequired,
dispatchFetchApplications: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);

View File

@@ -144,7 +144,7 @@ export const defaultState = {
},
category: function(item) {
if (item.categories.length > 0) {
if (item.categories !== undefined && item.categories.length > 0) {
const sortedCats = item.categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
const firstCat = sortedCats[0];

View File

@@ -1,4 +1,7 @@
@define-mixin scrollbar {
scrollbar-color: var(--scrollbarBackgroundColor) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 10px;
height: 10px;

View File

@@ -74,29 +74,29 @@ module.exports = {
defaultButtonTextColor: '#eee',
defaultButtonBackgroundColor: '#333',
defaultBorderColor: '#eaeaea',
defaultBorderColor: '#393f45',
defaultHoverBackgroundColor: '#444',
defaultHoverBorderColor: '#d6d6d6;',
defaultHoverBorderColor: '#5a6265',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7;',
primaryHoverBorderColor: '#3483e7',
successBackgroundColor: '#27c24c',
successBorderColor: '#26be4a',
successHoverBackgroundColor: '#24b145',
successHoverBorderColor: '#1f9c3d;',
successHoverBorderColor: '#1f9c3d',
warningBackgroundColor: '#ff902b',
warningBorderColor: '#ff8d26',
warningHoverBackgroundColor: '#ff8517',
warningHoverBorderColor: '#fc7800;',
warningHoverBorderColor: '#fc7800',
dangerBackgroundColor: '#f05050',
dangerBorderColor: '#f04b4b',
dangerHoverBackgroundColor: '#ee3d3d',
dangerHoverBorderColor: '#ec2626;',
dangerHoverBorderColor: '#ec2626',
iconButtonDisabledColor: '#7a7a7a',
iconButtonHoverColor: '#666',

View File

@@ -76,27 +76,27 @@ module.exports = {
defaultButtonBackgroundColor: '#fff',
defaultBorderColor: '#eaeaea',
defaultHoverBackgroundColor: '#f5f5f5',
defaultHoverBorderColor: '#d6d6d6;',
defaultHoverBorderColor: '#d6d6d6',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7;',
primaryHoverBorderColor: '#3483e7',
successBackgroundColor: '#27c24c',
successBorderColor: '#26be4a',
successHoverBackgroundColor: '#24b145',
successHoverBorderColor: '#1f9c3d;',
successHoverBorderColor: '#1f9c3d',
warningBackgroundColor: '#ff902b',
warningBorderColor: '#ff8d26',
warningHoverBackgroundColor: '#ff8517',
warningHoverBorderColor: '#fc7800;',
warningHoverBorderColor: '#fc7800',
dangerBackgroundColor: '#f05050',
dangerBorderColor: '#f04b4b',
dangerHoverBackgroundColor: '#ee3d3d',
dangerHoverBorderColor: '#ec2626;',
dangerHoverBorderColor: '#ec2626',
iconButtonDisabledColor: '#7a7a7a',
iconButtonHoverColor: '#666',

View File

@@ -5,7 +5,7 @@
"scripts": {
"build": "webpack --config ./frontend/build/webpack.config.js",
"prebuild": "yarn clean",
"clean": "rimraf ./_output/UI && rimraf \"**/*.js.map\"",
"clean": "rimraf ./_output/UI && rimraf -g \"**/*.js.map\"",
"start": "webpack --watch --config ./frontend/build/webpack.config.js",
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
@@ -20,42 +20,39 @@
"readmeFilename": "readme.md",
"main": "index.js",
"browserslist": [
">0.25%",
"not ie 11",
"not op_mini all",
"not chrome < 60"
"defaults"
],
"dependencies": {
"@fortawesome/fontawesome-free": "6.2.1",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/fontawesome-free": "6.4.0",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.13",
"@sentry/browser": "7.28.0",
"@sentry/integrations": "7.28.0",
"@types/jest": "29.2.5",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"chart.js": "4.1.1",
"@microsoft/signalr": "6.0.16",
"@sentry/browser": "7.46.0",
"@sentry/integrations": "7.46.0",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/react": "18.0.31",
"@types/react-dom": "18.0.11",
"chart.js": "4.2.1",
"classnames": "2.3.2",
"clipboard": "2.0.11",
"connected-react-router": "6.9.3",
"element-class": "0.2.2",
"filesize": "10.0.6",
"filesize": "10.0.7",
"history": "4.10.1",
"https-browserify": "1.0.0",
"jdu": "1.0.0",
"jquery": "3.6.2",
"jquery": "3.6.4",
"lodash": "4.17.21",
"mobile-detect": "1.4.5",
"moment": "2.29.4",
"mousetrap": "1.6.5",
"normalize.css": "8.0.1",
"prop-types": "15.8.1",
"qs": "6.11.0",
"qs": "6.11.1",
"react": "17.0.2",
"react-addons-shallow-compare": "15.6.3",
"react-async-script": "1.2.0",
@@ -67,7 +64,7 @@
"react-dnd-touch-backend": "14.1.1",
"react-document-title": "2.0.3",
"react-dom": "17.0.2",
"react-focus-lock": "2.9.2",
"react-focus-lock": "2.9.4",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0",
"react-measure": "1.4.7",
@@ -77,79 +74,78 @@
"react-router-dom": "5.2.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.1",
"react-virtualized": "9.21.1",
"react-virtualized": "9.22.3",
"react-window": "1.8.8",
"redux": "4.2.0",
"redux": "4.2.1",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
"redux-localstorage": "0.4.1",
"redux-thunk": "2.4.2",
"reselect": "4.1.7",
"stacktrace-js": "2.0.2",
"typescript": "4.9.4"
"typescript": "5.0.3"
},
"devDependencies": {
"@babel/core": "7.20.5",
"@babel/eslint-parser": "7.19.1",
"@babel/core": "7.21.3",
"@babel/eslint-parser": "7.21.3",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.20.5",
"@babel/plugin-proposal-decorators": "7.21.0",
"@babel/plugin-proposal-export-default-from": "7.18.10",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-function-sent": "7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-numeric-separator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.18.9",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-proposal-throw-expressions": "7.18.6",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.20.2",
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "7.18.6",
"@babel/preset-typescript": "7.21.0",
"@types/react-window": "1.8.5",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.0",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"are-you-es5": "2.1.2",
"autoprefixer": "10.4.13",
"babel-eslint": "10.1.0",
"babel-loader": "9.1.0",
"autoprefixer": "10.4.14",
"babel-loader": "9.1.2",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.26.1",
"core-js": "3.30.1",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.30.0",
"eslint-config-prettier": "8.6.0",
"eslint": "8.37.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "8.0.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "7.2.14",
"fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.0",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.2",
"postcss": "8.4.20",
"mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.21",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.0.2",
"postcss-loader": "7.1.0",
"postcss-mixins": "9.0.4",
"postcss-nested": "6.0.0",
"postcss-nested": "6.0.1",
"postcss-simple-vars": "7.0.1",
"postcss-url": "10.1.3",
"prettier": "2.8.2",
"prettier": "2.8.7",
"require-nocache": "1.0.0",
"rimraf": "3.0.2",
"rimraf": "4.4.1",
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "3.3.1",
"style-loader": "3.3.2",
"stylelint": "14.16.0",
"stylelint-order": "5.0.0",
"ts-loader": "9.4.2",
"typescript-plugin-css-modules": "4.1.1",
"typescript-plugin-css-modules": "5.0.0",
"url-loader": "4.1.1",
"webpack": "5.75.0",
"webpack-cli": "5.0.1",
"webpack": "5.82.0",
"webpack-cli": "5.0.2",
"webpack-livereload-plugin": "3.0.2"
}
}

View File

@@ -74,6 +74,8 @@
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<Deterministic Condition="$(AssemblyVersion.EndsWith('*'))">False</Deterministic>
<PathMap>$(MSBuildProjectDirectory)=./$(MSBuildProjectName)/</PathMap>
</PropertyGroup>
<!-- Set the AssemblyConfiguration attribute for projects -->

View File

@@ -1,4 +1,5 @@
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Test.Common;
@@ -12,14 +13,14 @@ namespace NzbDrone.Common.Test.EnsureTest
public void EnsureWindowsPath(string path)
{
WindowsOnly();
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
}
[TestCase(@"/var/user/file with, comma.mkv")]
public void EnsureLinuxPath(string path)
{
PosixOnly();
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NUnit.Framework;
using NzbDrone.Common.Http;
namespace NzbDrone.Common.Test.Http
{
[TestFixture]
public class CookieUtilFixture
{
[Test]
public void CookieHeaderToDictionaryGood()
{
// valid cookies with non-alpha characters in the value
var cookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA=";
var expectedCookieDictionary = new Dictionary<string, string>
{
{ "__cfduid", "d6237f041586694295" },
{ "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" }
};
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
}
[Test]
public void CookieHeaderToDictionaryDuplicateKeys()
{
// cookie with duplicate keys and whitespace separator instead of ;
var cookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA= __cf_bm=test";
var expectedCookieDictionary = new Dictionary<string, string>
{
{ "__cfduid", "d6237f041586694295" },
{ "__cf_bm", "test" } // we always assume the latest value is the most recent
};
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
}
[Test]
public void CookieHeaderToDictionaryMalformed()
{
// malformed cookies
var cookieHeader = "__cfduidd6237f041586694295; __cf_;bm TlOng; good_cookie=value";
var expectedCookieDictionary = new Dictionary<string, string> { { "good_cookie", "value" }, };
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(cookieHeader));
}
[Test]
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
public void CookieHeaderToDictionaryNull()
{
// null cookie header
var expectedCookieDictionary = new Dictionary<string, string>();
CollectionAssert.AreEqual(expectedCookieDictionary, CookieUtil.CookieHeaderToDictionary(null));
}
[Test]
public void CookieDictionaryToHeaderGood()
{
// valid cookies with non-alpha characters in the value
var cookieDictionary = new Dictionary<string, string>
{
{ "__cfduid", "d6237f041586694295" },
{ "__cf_bm", "TlOng/xyqckk-TMen38z+0RFYA7YA=" }
};
var expectedCookieHeader = "__cfduid=d6237f041586694295; __cf_bm=TlOng/xyqckk-TMen38z+0RFYA7YA=";
CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(cookieDictionary));
}
[Test]
public void CookieDictionaryToHeaderMalformed1()
{
// malformed key
var cookieDictionary = new Dictionary<string, string>
{
{ "__cf_=bm", "34234234" }
};
var ex = Assert.Throws<FormatException>(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary));
Assert.AreEqual("The cookie '__cf_=bm=34234234' is malformed.", ex.Message);
}
[Test]
public void CookieDictionaryToHeaderMalformed2()
{
// malformed value
var cookieDictionary = new Dictionary<string, string>
{
{ "__cf_bm", "34234 234" }
};
var ex = Assert.Throws<FormatException>(() => CookieUtil.CookieDictionaryToHeader(cookieDictionary));
Assert.AreEqual("The cookie '__cf_bm=34234 234' is malformed.", ex.Message);
}
[Test]
public void CookieDictionaryToHeaderNull()
{
// null cookie dictionary
var expectedCookieHeader = "";
CollectionAssert.AreEqual(expectedCookieHeader, CookieUtil.CookieDictionaryToHeader(null));
}
}
}

View File

@@ -35,6 +35,7 @@ namespace NzbDrone.Common.Test
[TestCase(@"\\Testserver\Test\file.ext", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext\\", @"\\Testserver\Test\file.ext")]
[TestCase(@"\\Testserver\Test\file.ext \\", @"\\Testserver\Test\file.ext")]
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
public void Clean_Path_Windows(string dirty, string clean)
{
WindowsOnly();

View File

@@ -5,8 +5,6 @@ using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Composition
@@ -19,16 +17,17 @@ namespace NzbDrone.Common.Composition
RegisterSQLiteResolver();
}
public static IEnumerable<Assembly> Load(IEnumerable<string> assemblies)
public static IList<Assembly> Load(IList<string> assemblyNames)
{
var toLoad = assemblies.ToList();
var toLoad = assemblyNames.ToList();
toLoad.Add("Prowlarr.Common");
toLoad.Add(OsInfo.IsWindows ? "Prowlarr.Windows" : "Prowlarr.Mono");
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
return toLoad.Select(x =>
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")));
return toLoad
.Select(x => AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")))
.ToList();
}
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)

View File

@@ -65,7 +65,7 @@ namespace NzbDrone.Common.Disk
private void CheckFolderExists(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
if (!FolderExists(path))
{
@@ -75,7 +75,7 @@ namespace NzbDrone.Common.Disk
private void CheckFileExists(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
if (!FileExists(path))
{
@@ -93,19 +93,19 @@ namespace NzbDrone.Common.Disk
public bool FolderExists(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.Exists(path);
}
public bool FileExists(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return FileExists(path, PathStringComparison);
}
public bool FileExists(string path, StringComparison stringComparison)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
switch (stringComparison)
{
@@ -125,7 +125,7 @@ namespace NzbDrone.Common.Disk
public bool FolderWritable(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
try
{
@@ -144,35 +144,35 @@ namespace NzbDrone.Common.Disk
public bool FolderEmpty(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.EnumerateFileSystemEntries(path).Empty();
}
public string[] GetDirectories(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.GetDirectories(path);
}
public string[] GetFiles(string path, SearchOption searchOption)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.GetFiles(path, "*.*", searchOption);
}
public long GetFolderSize(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return GetFiles(path, SearchOption.AllDirectories).Sum(e => new FileInfo(e).Length);
}
public long GetFileSize(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
if (!FileExists(path))
{
@@ -185,13 +185,13 @@ namespace NzbDrone.Common.Disk
public void CreateFolder(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
Directory.CreateDirectory(path);
}
public void DeleteFile(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
Logger.Trace("Deleting file: {0}", path);
RemoveReadOnly(path);
@@ -201,8 +201,8 @@ namespace NzbDrone.Common.Disk
public void CloneFile(string source, string destination, bool overwrite = false)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
if (source.PathEquals(destination))
{
@@ -219,8 +219,8 @@ namespace NzbDrone.Common.Disk
public void CopyFile(string source, string destination, bool overwrite = false)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
if (source.PathEquals(destination))
{
@@ -237,8 +237,8 @@ namespace NzbDrone.Common.Disk
public void MoveFile(string source, string destination, bool overwrite = false)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
if (source.PathEquals(destination))
{
@@ -256,8 +256,8 @@ namespace NzbDrone.Common.Disk
public void MoveFolder(string source, string destination, bool overwrite = false)
{
Ensure.That(source, () => source).IsValidPath();
Ensure.That(destination, () => destination).IsValidPath();
Ensure.That(source, () => source).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(destination, () => destination).IsValidPath(PathValidationType.CurrentOs);
Directory.Move(source, destination);
}
@@ -281,7 +281,7 @@ namespace NzbDrone.Common.Disk
public void DeleteFolder(string path, bool recursive)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
Array.ForEach(files, RemoveReadOnly);
@@ -291,14 +291,14 @@ namespace NzbDrone.Common.Disk
public string ReadAllText(string filePath)
{
Ensure.That(filePath, () => filePath).IsValidPath();
Ensure.That(filePath, () => filePath).IsValidPath(PathValidationType.CurrentOs);
return File.ReadAllText(filePath);
}
public void WriteAllText(string filename, string contents)
{
Ensure.That(filename, () => filename).IsValidPath();
Ensure.That(filename, () => filename).IsValidPath(PathValidationType.CurrentOs);
RemoveReadOnly(filename);
// File.WriteAllText is broken on net core when writing to some CIFS mounts
@@ -314,7 +314,7 @@ namespace NzbDrone.Common.Disk
public void FolderSetLastWriteTime(string path, DateTime dateTime)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
if (dateTime.Before(DateTimeExtensions.Epoch))
{
@@ -326,7 +326,7 @@ namespace NzbDrone.Common.Disk
public void FileSetLastWriteTime(string path, DateTime dateTime)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
if (dateTime.Before(DateTimeExtensions.Epoch))
{
@@ -351,16 +351,16 @@ namespace NzbDrone.Common.Disk
}
}
public string GetPathRoot(string path)
public virtual string GetPathRoot(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Path.GetPathRoot(path);
}
public string GetParentFolder(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var parent = Directory.GetParent(path.TrimEnd(Path.DirectorySeparatorChar));
@@ -407,7 +407,7 @@ namespace NzbDrone.Common.Disk
public void EmptyFolder(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
foreach (var file in GetFiles(path, SearchOption.TopDirectoryOnly))
{
@@ -478,8 +478,7 @@ namespace NzbDrone.Common.Disk
return mounts.Where(drive => drive.RootDirectory.PathEquals(path) ||
drive.RootDirectory.IsParentPath(path))
.OrderByDescending(drive => drive.RootDirectory.Length)
.FirstOrDefault();
.MaxBy(drive => drive.RootDirectory.Length);
}
catch (Exception ex)
{
@@ -497,7 +496,7 @@ namespace NzbDrone.Common.Disk
public List<DirectoryInfo> GetDirectoryInfos(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var di = new DirectoryInfo(path);
@@ -506,14 +505,14 @@ namespace NzbDrone.Common.Disk
public FileInfo GetFileInfo(string path)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return new FileInfo(path);
}
public List<FileInfo> GetFileInfos(string path, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
var di = new DirectoryInfo(path);

View File

@@ -43,8 +43,8 @@ namespace NzbDrone.Common.Disk
public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode)
{
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
@@ -140,8 +140,8 @@ namespace NzbDrone.Common.Disk
{
var filesCopied = 0;
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);
@@ -255,8 +255,8 @@ namespace NzbDrone.Common.Disk
public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false)
{
Ensure.That(sourcePath, () => sourcePath).IsValidPath();
Ensure.That(targetPath, () => targetPath).IsValidPath();
Ensure.That(sourcePath, () => sourcePath).IsValidPath(PathValidationType.CurrentOs);
Ensure.That(targetPath, () => targetPath).IsValidPath(PathValidationType.CurrentOs);
sourcePath = ResolveRealParentPath(sourcePath);
targetPath = ResolveRealParentPath(targetPath);

View File

@@ -71,7 +71,7 @@ namespace NzbDrone.Common.Disk
if (
allowFoldersWithoutTrailingSlashes &&
query.IsPathValid() &&
query.IsPathValid(PathValidationType.CurrentOs) &&
_diskProvider.FolderExists(query))
{
return GetResult(query, includeFiles);

View File

@@ -162,7 +162,7 @@ namespace NzbDrone.Common.Disk
}
}
public bool IsValid => _path.IsPathValid();
public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs);
private int GetFileNameIndex()
{

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Common.Disk
{
public enum PathValidationType
{
CurrentOs,
AnyOs
}
}

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat.Resources;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -111,14 +112,14 @@ namespace NzbDrone.Common.EnsureThat
}
[DebuggerStepThrough]
public static Param<string> IsValidPath(this Param<string> param)
public static Param<string> IsValidPath(this Param<string> param, PathValidationType validationType)
{
if (string.IsNullOrWhiteSpace(param.Value))
{
throw ExceptionFactory.CreateForParamValidation(param.Name, ExceptionMessages.EnsureExtensions_IsNotNullOrWhiteSpace);
}
if (param.Value.IsPathValid())
if (param.Value.IsPathValid(validationType))
{
return param;
}

View File

@@ -29,12 +29,12 @@ namespace NzbDrone.Common.Extensions
public static string CleanFilePath(this string path)
{
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
Ensure.That(path, () => path).IsValidPath();
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
var info = new FileInfo(path.Trim());
// UNC
if (OsInfo.IsWindows && info.FullName.StartsWith(@"\\"))
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
{
return info.FullName.TrimEnd('/', '\\', ' ');
}
@@ -136,24 +136,24 @@ namespace NzbDrone.Common.Extensions
private static readonly Regex WindowsPathWithDriveRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled);
public static bool IsPathValid(this string path)
public static bool IsPathValid(this string path, PathValidationType validationType)
{
if (path.ContainsInvalidPathChars() || string.IsNullOrWhiteSpace(path))
{
return false;
}
if (validationType == PathValidationType.AnyOs)
{
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
}
if (OsInfo.IsNotWindows)
{
return path.StartsWith(Path.DirectorySeparatorChar.ToString());
return IsPathValidForNonWindows(path);
}
if (path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path))
{
return true;
}
return false;
return IsPathValidForWindows(path);
}
public static bool ContainsInvalidPathChars(this string text)
@@ -337,5 +337,15 @@ namespace NzbDrone.Common.Extensions
{
return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE);
}
private static bool IsPathValidForWindows(string path)
{
return path.StartsWith("\\") || WindowsPathWithDriveRegex.IsMatch(path);
}
private static bool IsPathValidForNonWindows(string path)
{
return path.StartsWith("/");
}
}
}

View File

@@ -16,18 +16,7 @@ namespace NzbDrone.Common.Extensions
return false;
}
Uri uri;
if (!Uri.TryCreate(path, UriKind.Absolute, out uri))
{
return false;
}
if (!uri.IsWellFormedOriginalString())
{
return false;
}
return true;
return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsWellFormedOriginalString();
}
}
}

View File

@@ -29,6 +29,14 @@ namespace NzbDrone.Common
return $"{mCrc:x8}";
}
public static string ComputeSha256Hash(string rawData)
{
using var sha256Hash = SHA256.Create();
var hashBytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
return Convert.ToHexString(hashBytes);
}
public static string CalculateMd5(string s)
{
// Use input string to calculate MD5 hash

View File

@@ -9,7 +9,7 @@ namespace NzbDrone.Common.Http
{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
// NOTE: we are not checking non-ascii characters and we should
private static readonly Regex _CookieRegex = new Regex(@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly Regex CookieRegex = new (@"([^\(\)<>@,;:\\""/\[\]\?=\{\}\s]+)=([^,;\\""\s]+)");
private static readonly string[] FilterProps = { "COMMENT", "COMMENTURL", "DISCORD", "DOMAIN", "EXPIRES", "MAX-AGE", "PATH", "PORT", "SECURE", "VERSION", "HTTPONLY", "SAMESITE" };
private static readonly char[] InvalidKeyChars = { '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t', '\n' };
private static readonly char[] InvalidValueChars = { '"', ',', ';', '\\', ' ', '\t', '\n' };
@@ -22,7 +22,7 @@ namespace NzbDrone.Common.Http
return cookieDictionary;
}
var matches = _CookieRegex.Match(cookieHeader);
var matches = CookieRegex.Match(cookieHeader);
while (matches.Success)
{
if (matches.Groups.Count > 2 && !FilterProps.Contains(matches.Groups[1].Value.ToUpperInvariant()))

View File

@@ -21,6 +21,7 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
@@ -108,6 +109,7 @@ namespace NzbDrone.Common.Http
request.Method = Method;
request.Encoding = Encoding;
request.SuppressHttpError = SuppressHttpError;
request.SuppressHttpErrorStatusCodes = SuppressHttpErrorStatusCodes;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect;

View File

@@ -14,14 +14,14 @@ namespace NzbDrone.Common.OAuth
{
get
{
var parameters = this.Where(p => p.Name.Equals(name));
var parameters = this.Where(p => p.Name.Equals(name)).ToArray();
if (!parameters.Any())
{
return null;
}
if (parameters.Count() == 1)
if (parameters.Length == 1)
{
return parameters.Single();
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;

View File

@@ -4,16 +4,16 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.3.3" />
<PackageReference Include="DryIoc.dll" Version="5.3.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Npgsql" Version="5.0.11" />
<PackageReference Include="Sentry" Version="3.24.1" />
<PackageReference Include="Sentry" Version="3.29.1" />
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />

View File

@@ -0,0 +1,826 @@
{
"Results": 9999,
"Pagination": {
"Current": 1,
"Max": 99,
"Limit": {
"Min": 15,
"Coerced": 15,
"Max": 50
}
},
"Matches": 2,
"Groups": [
{
"ID": 575,
"CategoryName": "Anime",
"FullName": "Cowboy Bebop: Tengoku no Tobira - Movie&nbsp;&nbsp;[2001]",
"GroupName": "Movie",
"SeriesID": "141",
"SeriesName": "Cowboy Bebop: Tengoku no Tobira",
"Artists": null,
"Year": "2001",
"Image": "https://mei.animebytes.tv/2bac1a04148be77ce41251d3cb44bbd5.jpg",
"Synonymns": [
"カウボーイビバップ天国の扉",
"Cowboy Bebop: Knockin' on Heaven's Door"
],
"SynonymnsV2": {
"Japanese": "カウボーイビバップ天国の扉",
"Romaji": "",
"Alternative": "Cowboy Bebop: Knockin' on Heaven's Door"
},
"Snatched": 4900,
"Comments": 11,
"Links": {
"AniDB": "https://anidb.net/anime/219",
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=353",
"Wikipedia": "https://en.wikipedia.org/wiki/Cowboy_Bebop:_The_Movie",
"MAL": "https://myanimelist.net/anime/5"
},
"Votes": 572,
"AvgVote": 8.3,
"Associations": null,
"Description": "Mars is under siege! Just before Halloween 2071, a terrorist bomb destroys a tanker truck on Highway One, close to the densely-populated crater city. There are casualties up to half a mile from the blast — 500 killed or injured by what appears to be a biochemical weapon. The reward for the bomber's capture is a massive 300,000,000 woolongs... and there are four humans and a dog who really need the money. Down on their luck as usual, the crew of the Bebop get on the case.\r\n\r\n[i]Note: The movie takes place between (in the time period of) the Cowboy Bebop episodes 22 and 23.[/i]",
"DescriptionHTML": "Mars is under siege! Just before Halloween 2071, a terrorist bomb destroys a tanker truck on Highway One, close to the densely-populated crater city. There are casualties up to half a mile from the blast — 500 killed or injured by what appears to be a biochemical weapon. The reward for the bomber&#039;s capture is a massive 300,000,000 woolongs... and there are four humans and a dog who really need the money. Down on their luck as usual, the crew of the Bebop get on the case.<br />\r\n<br />\r\n<em>Note: The movie takes place between (in the time period of) the Cowboy Bebop episodes 22 and 23.</em>",
"EpCount": 0,
"StudioList": "Sunrise///28|BONES///35",
"PastWeek": 2,
"Incomplete": false,
"Ongoing": false,
"Tags": [
"adventure",
"comedy",
"drama",
"scifi",
"seinen",
"action"
],
"Torrents": [
{
"ID": 959397,
"EditionData": {
"EditionTitle": ""
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/959397/download/somepass",
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | Opus 5.1 | Softsubs (Polarwindz) | Freeleech",
"Snatched": 16,
"Seeders": 5,
"Leechers": 1,
"Status": 0,
"Size": 13090646841,
"FileCount": 1,
"FileList": [
{
"filename": "[Polarwindz] Cowboy Bebop The Movie - Knockin' on Heaven's Door [BD 1080p x265 10bit Opus 5.1].mkv",
"size": 13090646841
}
],
"UploadTime": "2023-04-02 05:00:43"
},
{
"ID": 909565,
"EditionData": {
"EditionTitle": ""
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/909565/download/somepass",
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | Opus 5.1 | Dual Audio | Softsubs (Yūrei) | Freeleech",
"Snatched": 29,
"Seeders": 6,
"Leechers": 0,
"Status": 0,
"Size": 15717521349,
"FileCount": 1,
"FileList": [
{
"filename": "Cowboy Bebop - Tengoku no Tobira.mkv",
"size": 15717521349
}
],
"UploadTime": "2020-09-03 18:04:38"
}
]
},
{
"ID": 2709,
"CategoryName": "Anime",
"FullName": "BLEACH - TV Series&nbsp;&nbsp;[2004]",
"GroupName": "TV Series",
"SeriesID": "191",
"SeriesName": "BLEACH",
"Artists": null,
"Year": "2004",
"Image": "https://mei.animebytes.tv/997c8ec3ca0e70254b182b0a176f0161.jpg",
"Synonymns": [
"ブリーチ"
],
"SynonymnsV2": {
"Japanese": "ブリーチ",
"Romaji": "",
"Alternative": ""
},
"Snatched": 22653,
"Comments": 51,
"Links": {
"AniDB": "https://anidb.net/anime/2369",
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=4240",
"Wikipedia": "https://en.wikipedia.org/wiki/Bleach_(anime)",
"MAL": "https://myanimelist.net/anime/269"
},
"Votes": 530,
"AvgVote": 7.3,
"Associations": null,
"Description": "Kurosaki Ichigo is a teenager gifted with the ability to see spirits. His life is drastically changed by the sudden appearance of a Shinigami (literally, Death god) - one who governs the flow of souls between the human world and the afterlife - named Kuchiki Rukia, who arrives in search of a Hollow, a dangerous lost soul. When Rukia is severely wounded while trying to defeat the Hollow, she attempts to transfer half of her Reiatsu (literally, Spiritual pressure) energy to Ichigo so that he can defeat the Hollow. However, Ichigo takes almost all of her energy, transforming into a Shinigami and allowing him to defeat the Hollow with ease. With her powers diminished, Rukia is left stranded in the human world until she can recover her strength. In the meantime, Ichigo must take over Rukia's role as a Shinigami, battling Hollows and guiding souls to the afterlife realm known as the Soul Society.",
"DescriptionHTML": "Kurosaki Ichigo is a teenager gifted with the ability to see spirits. His life is drastically changed by the sudden appearance of a Shinigami (literally, Death god) - one who governs the flow of souls between the human world and the afterlife - named Kuchiki Rukia, who arrives in search of a Hollow, a dangerous lost soul. When Rukia is severely wounded while trying to defeat the Hollow, she attempts to transfer half of her Reiatsu (literally, Spiritual pressure) energy to Ichigo so that he can defeat the Hollow. However, Ichigo takes almost all of her energy, transforming into a Shinigami and allowing him to defeat the Hollow with ease. With her powers diminished, Rukia is left stranded in the human world until she can recover her strength. In the meantime, Ichigo must take over Rukia&#039;s role as a Shinigami, battling Hollows and guiding souls to the afterlife realm known as the Soul Society.",
"EpCount": 366,
"StudioList": "Studio Pierrot///45",
"PastWeek": 26,
"Incomplete": false,
"Ongoing": false,
"Tags": [
"adventure",
"comedy",
"fantasy",
"martial.arts",
"school.life",
"shounen",
"super.power",
"contemporary.fantasy",
"swordplay",
"action",
"supernatural"
],
"Torrents": [
{
"ID": 1031199,
"EditionData": {
"EditionTitle": "Season 02: The Entry (021-041)"
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/1031199/download/somepass",
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | AAC 2.0 | Dual Audio | Softsubs (GHOST) | Freeleech",
"Snatched": 20,
"Seeders": 24,
"Leechers": 0,
"Status": 0,
"Size": 19584943785,
"FileCount": 21,
"FileList": [
{
"filename": "[GHOST][1080p] Bleach - 021 [BD HEVC 10bit Dual Audio AC3][035452C3].mkv",
"size": 880693454
},
{
"filename": "[GHOST][1080p] Bleach - 022 [BD HEVC 10bit Dual Audio AC3][0E923AAD].mkv",
"size": 851531918
},
{
"filename": "[GHOST][1080p] Bleach - 023 [BD HEVC 10bit Dual Audio AC3][604A0EC6].mkv",
"size": 882518038
},
{
"filename": "[GHOST][1080p] Bleach - 024 [BD HEVC 10bit Dual Audio AC3][ABABE9B3].mkv",
"size": 837335522
},
{
"filename": "[GHOST][1080p] Bleach - 025 [BD HEVC 10bit Dual Audio AC3][17AEB0C1].mkv",
"size": 933034706
},
{
"filename": "[GHOST][1080p] Bleach - 026 [BD HEVC 10bit Dual Audio AC3][E6F0017C].mkv",
"size": 891916996
},
{
"filename": "[GHOST][1080p] Bleach - 027 [BD HEVC 10bit Dual Audio AC3][317791C7].mkv",
"size": 896856044
},
{
"filename": "[GHOST][1080p] Bleach - 028 [BD HEVC 10bit Dual Audio AC3][5CA4DF06].mkv",
"size": 1010510386
},
{
"filename": "[GHOST][1080p] Bleach - 029 [BD HEVC 10bit Dual Audio AC3][549CF25A].mkv",
"size": 995648398
},
{
"filename": "[GHOST][1080p] Bleach - 030 [BD HEVC 10bit Dual Audio AC3][CED479F9].mkv",
"size": 991633973
},
{
"filename": "[GHOST][1080p] Bleach - 031 [BD HEVC 10bit Dual Audio AC3][1EEFAB0E].mkv",
"size": 950195341
},
{
"filename": "[GHOST][1080p] Bleach - 032 [BD HEVC 10bit Dual Audio AC3][C6F3C39A].mkv",
"size": 859218784
},
{
"filename": "[GHOST][1080p] Bleach - 033 [BD HEVC 10bit Dual Audio AC3][A8424897].mkv",
"size": 933230823
},
{
"filename": "[GHOST][1080p] Bleach - 034 [BD HEVC 10bit Dual Audio AC3][96C94DB8].mkv",
"size": 818179634
},
{
"filename": "[GHOST][1080p] Bleach - 035 [BD HEVC 10bit Dual Audio AC3][A07831AE].mkv",
"size": 882457138
},
{
"filename": "[GHOST][1080p] Bleach - 036 [BD HEVC 10bit Dual Audio AC3][D984C169].mkv",
"size": 895683290
},
{
"filename": "[GHOST][1080p] Bleach - 037 [BD HEVC 10bit Dual Audio AC3][32C05A7E].mkv",
"size": 970033716
},
{
"filename": "[GHOST][1080p] Bleach - 038 [BD HEVC 10bit Dual Audio AC3][824B3EEC].mkv",
"size": 956573899
},
{
"filename": "[GHOST][1080p] Bleach - 039 [BD HEVC 10bit Dual Audio AC3][A45233DF].mkv",
"size": 1238240707
},
{
"filename": "[GHOST][1080p] Bleach - 040 [BD HEVC 10bit Dual Audio AC3][5334A7F1].mkv",
"size": 894491562
},
{
"filename": "[GHOST][1080p] Bleach - 041 [BD HEVC 10bit Dual Audio AC3][A4F17363].mkv",
"size": 1014959456
}
],
"UploadTime": "2023-03-03 03:03:17"
},
{
"ID": 1031203,
"EditionData": {
"EditionTitle": "Season 03: The Rescue (042-063)"
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/1031203/download/somepass",
"Property": "Blu-ray | MKV | h265 10-bit | 1080p | AC3 2.0 | Dual Audio | Softsubs (GHOST) | Freeleech",
"Snatched": 6,
"Seeders": 12,
"Leechers": 2,
"Status": 0,
"Size": 24498538059,
"FileCount": 22,
"FileList": [
{
"filename": "[GHOST][1080p] Bleach - 042 [BD HEVC 10bit Dual Audio AC3][55763BF6].mkv",
"size": 899825828
},
{
"filename": "[GHOST][1080p] Bleach - 043 [BD HEVC 10bit Dual Audio AC3][70B71ECC].mkv",
"size": 902256094
},
{
"filename": "[GHOST][1080p] Bleach - 044 [BD HEVC 10bit Dual Audio AC3][35F5526B].mkv",
"size": 885323344
},
{
"filename": "[GHOST][1080p] Bleach - 045 [BD HEVC 10bit Dual Audio AC3][9C1DAE4E].mkv",
"size": 1082465042
},
{
"filename": "[GHOST][1080p] Bleach - 046 [BD HEVC 10bit Dual Audio AC3][869EF5B6].mkv",
"size": 957433930
},
{
"filename": "[GHOST][1080p] Bleach - 047 [BD HEVC 10bit Dual Audio AC3][890DA7CE].mkv",
"size": 997124540
},
{
"filename": "[GHOST][1080p] Bleach - 048 [BD HEVC 10bit Dual Audio AC3][39064E08].mkv",
"size": 1026751383
},
{
"filename": "[GHOST][1080p] Bleach - 049 [BD HEVC 10bit Dual Audio AC3][C536D3DB].mkv",
"size": 994918391
},
{
"filename": "[GHOST][1080p] Bleach - 050 [BD HEVC 10bit Dual Audio AC3][A7A0CB24].mkv",
"size": 1141146920
},
{
"filename": "[GHOST][1080p] Bleach - 051 [BD HEVC 10bit Dual Audio AC3][25C06D9D].mkv",
"size": 951751791
},
{
"filename": "[GHOST][1080p] Bleach - 052 [BD HEVC 10bit Dual Audio AC3][FB506194].mkv",
"size": 1131756065
},
{
"filename": "[GHOST][1080p] Bleach - 053 [BD HEVC 10bit Dual Audio AC3][4A76C66D].mkv",
"size": 1076952986
},
{
"filename": "[GHOST][1080p] Bleach - 054 [BD HEVC 10bit Dual Audio AC3][51D8E5F8].mkv",
"size": 1369454462
},
{
"filename": "[GHOST][1080p] Bleach - 055 [BD HEVC 10bit Dual Audio AC3][DCF20007].mkv",
"size": 1428073116
},
{
"filename": "[GHOST][1080p] Bleach - 056 [BD HEVC 10bit Dual Audio AC3][34A28687].mkv",
"size": 1304804717
},
{
"filename": "[GHOST][1080p] Bleach - 057 [BD HEVC 10bit Dual Audio AC3][D1D5FE29].mkv",
"size": 1056220730
},
{
"filename": "[GHOST][1080p] Bleach - 058 [BD HEVC 10bit Dual Audio AC3][C6EAC278].mkv",
"size": 1551455953
},
{
"filename": "[GHOST][1080p] Bleach - 059 [BD HEVC 10bit Dual Audio AC3][E7B25869].mkv",
"size": 1026910923
},
{
"filename": "[GHOST][1080p] Bleach - 060 [BD HEVC 10bit Dual Audio AC3][C9D257D4].mkv",
"size": 1059631177
},
{
"filename": "[GHOST][1080p] Bleach - 061 [BD HEVC 10bit Dual Audio AC3][0521C8D3].mkv",
"size": 1457893287
},
{
"filename": "[GHOST][1080p] Bleach - 062 [BD HEVC 10bit Dual Audio AC3][65CBA616].mkv",
"size": 1262863946
},
{
"filename": "[GHOST][1080p] Bleach - 063 [BD HEVC 10bit Dual Audio AC3][CF63E244].mkv",
"size": 933523434
}
],
"UploadTime": "2023-04-03 03:14:35"
}
]
},
{
"ID": 81926,
"CategoryName": "Anime",
"FullName": "Dr. STONE: NEW WORLD - TV Series&nbsp;&nbsp;[2023]",
"GroupName": "TV Series",
"SeriesID": "79217",
"SeriesName": "Dr. STONE: NEW WORLD",
"Artists": null,
"Year": "2023",
"Image": "https://mei.animebytes.tv/Tu0p0k56514.jpg",
"Synonymns": {
"0": "ドクターストーン NEW WORLD",
"2": "Dr. STONE S3, Dr. STONE Season 3, Dr.STONE 3rd Season"
},
"SynonymnsV2": {
"Japanese": "ドクターストーン NEW WORLD",
"Romaji": "",
"Alternative": "Dr. STONE S3, Dr. STONE Season 3, Dr.STONE 3rd Season"
},
"Snatched": 1870,
"Comments": 0,
"Links": {
"AniDB": "https://anidb.net/anime/17053",
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=25068",
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
"MAL": "https://myanimelist.net/anime/48549"
},
"Votes": 0,
"AvgVote": 0,
"Associations": null,
"Description": "Third season of [i]Dr. STONE[/i].\r\n\r\nWith the Stone Wars over, the former members of Tsukasa's Empire of Might join forces with the Kingdom of Science to build a ship capable of sailing across the open ocean to seek answers to the mystery of global petrification. However, before they can begin their voyage Senku and his friends need to find some key resources and push some new scientific advancements to build the type of vessel they need.\r\n\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.\r\n\r\n[i]Note: The first episode received an early screening at a special event on March 12, 2023 at Iino Hall in Tokyo. The regular TV broadcast started on April 6, 2023.[/i]",
"DescriptionHTML": "Third season of <em>Dr. STONE</em>.<br />\r\n<br />\r\nWith the Stone Wars over, the former members of Tsukasa&#039;s Empire of Might join forces with the Kingdom of Science to build a ship capable of sailing across the open ocean to seek answers to the mystery of global petrification. However, before they can begin their voyage Senku and his friends need to find some key resources and push some new scientific advancements to build the type of vessel they need.<br />\r\n<br />\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.<br />\r\n<br />\r\n<em>Note: The first episode received an early screening at a special event on March 12, 2023 at Iino Hall in Tokyo. The regular TV broadcast started on April 6, 2023.</em>",
"EpCount": 0,
"StudioList": "TMS Entertainment///11",
"PastWeek": 6,
"Incomplete": false,
"Ongoing": false,
"Tags": [
"adventure",
"scifi",
"shounen",
"post.apocalyptic"
],
"Torrents": [
{
"ID": 1041495,
"EditionData": {
"EditionTitle": "Episode 3"
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/1041495/download/somepass",
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 3 | Freeleech",
"Snatched": 165,
"Seeders": 137,
"Leechers": 3,
"Status": 0,
"Size": 748209543,
"FileCount": 1,
"FileList": [
{
"filename": "[SubsPlease] Dr. Stone S3 - 03 (720p) [DAC92E18].mkv",
"size": 748209543
}
],
"UploadTime": "2023-04-20 14:32:29"
},
{
"ID": 1037731,
"EditionData": {
"EditionTitle": "Episode 2"
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/1037731/download/somepass",
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 2 | Freeleech",
"Snatched": 174,
"Seeders": 122,
"Leechers": 1,
"Status": 0,
"Size": 748808730,
"FileCount": 1,
"FileList": [
{
"filename": "[SubsPlease] Dr. Stone S3 - 02 (720p) [AE2DA9AB].mkv",
"size": 748808730
}
],
"UploadTime": "2023-04-13 14:34:16"
}
]
},
{
"ID": 69267,
"CategoryName": "Anime",
"FullName": "Dr. STONE: STONE WARS - TV Series&nbsp;&nbsp;[2021]",
"GroupName": "TV Series",
"SeriesID": "67161",
"SeriesName": "Dr. STONE: STONE WARS",
"Artists": null,
"Year": "2021",
"Image": "https://mei.animebytes.tv/6pqXEK82OfD.jpg",
"Synonymns": {
"0": "ドクターストーン STONE WARS",
"2": "Dr. STONE S2, Dr. STONE Season 2, Dr.STONE 2nd Season"
},
"SynonymnsV2": {
"Japanese": "ドクターストーン STONE WARS",
"Romaji": "",
"Alternative": "Dr. STONE S2, Dr. STONE Season 2, Dr.STONE 2nd Season"
},
"Snatched": 1181,
"Comments": 4,
"Links": {
"AniDB": "https://anidb.net/anime/15305",
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=22942",
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
"MAL": "https://myanimelist.net/anime/40852/Dr_Stone__Stone_Wars"
},
"Votes": 23,
"AvgVote": 7.8,
"Associations": null,
"Description": "* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.\r\n\r\nSenkuu, Chrome and the other villagers are in a battle of wits and brawn against the Tsukasa Empire after the revelation that Senkuu's father left behind a lasting message.\r\n\r\nWith a plan to build a phone that could be used to take down the bad guys from within, Senkuu will need to finally enlist the help of his friends who are a part of the Tsukasa Empire.",
"DescriptionHTML": "* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.<br />\r\n<br />\r\nSenkuu, Chrome and the other villagers are in a battle of wits and brawn against the Tsukasa Empire after the revelation that Senkuu&#039;s father left behind a lasting message.<br />\r\n<br />\r\nWith a plan to build a phone that could be used to take down the bad guys from within, Senkuu will need to finally enlist the help of his friends who are a part of the Tsukasa Empire.",
"EpCount": 11,
"StudioList": "TMS Entertainment///11",
"PastWeek": 0,
"Incomplete": false,
"Ongoing": false,
"Tags": [
"adventure",
"scifi",
"shounen",
"post.apocalyptic"
],
"Torrents": [
{
"ID": 944509,
"EditionData": {
"EditionTitle": ""
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/944509/download/somepass",
"Property": "Web | MKV | h264 | 1080p | AAC 2.0 | Dual Audio | Softsubs (-ZR-) | Freeleech",
"Snatched": 188,
"Seeders": 31,
"Leechers": 1,
"Status": 0,
"Size": 16611719364,
"FileCount": 11,
"FileList": [
{
"filename": "Dr. Stone S02E01v2 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1512195256
},
{
"filename": "Dr. Stone S02E02 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1507917714
},
{
"filename": "Dr. Stone S02E03 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1510054199
},
{
"filename": "Dr. Stone S02E04 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1507100461
},
{
"filename": "Dr. Stone S02E05 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1507258273
},
{
"filename": "Dr. Stone S02E06 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1511039711
},
{
"filename": "Dr. Stone S02E07 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1507219047
},
{
"filename": "Dr. Stone S02E08 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1510996213
},
{
"filename": "Dr. Stone S02E09 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1512785600
},
{
"filename": "Dr. Stone S02E10 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1511889715
},
{
"filename": "Dr. Stone S02E11 2021 1080p WEB-DL AVC AAC 2.0 Dual Audio -ZR-.mkv",
"size": 1513263175
}
],
"UploadTime": "2021-06-03 20:30:00"
}
]
},
{
"ID": 60598,
"CategoryName": "Anime",
"FullName": "Dr. STONE - TV Series&nbsp;&nbsp;[2019]",
"GroupName": "TV Series",
"SeriesID": "56617",
"SeriesName": "Dr. STONE",
"Artists": null,
"Year": "2019",
"Image": "https://mei.animebytes.tv/SaFez5XG8T3.jpg",
"Synonymns": [
"Dr.STONE [ドクターストーン]"
],
"SynonymnsV2": {
"Japanese": "Dr.STONE [ドクターストーン]",
"Romaji": "",
"Alternative": ""
},
"Snatched": 2174,
"Comments": 8,
"Links": {
"AniDB": "https://anidb.net/anime/14491",
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=21703",
"Wikipedia": "https://en.wikipedia.org/wiki/Dr._Stone",
"MAL": "https://myanimelist.net/anime/38691"
},
"Votes": 68,
"AvgVote": 7.9,
"Associations": null,
"Description": "Several thousand years after a mysterious phenomenon that turns all of humanity to stone, the extraordinarily intelligent, science-driven boy, Senku Ishigami, awakens. Facing a world of stone and the total collapse of civilization, Senku makes up his mind to use science to rebuild the world. Starting with his super strong childhood friend Taiju Oki, who awakened at the same time, they will begin to rebuild civilization from nothing... Depicting two million years of scientific history from the Stone Age to present day, the unprecedented crafting adventure story is about to begin!\r\n\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.",
"DescriptionHTML": "Several thousand years after a mysterious phenomenon that turns all of humanity to stone, the extraordinarily intelligent, science-driven boy, Senku Ishigami, awakens. Facing a world of stone and the total collapse of civilization, Senku makes up his mind to use science to rebuild the world. Starting with his super strong childhood friend Taiju Oki, who awakened at the same time, they will begin to rebuild civilization from nothing... Depicting two million years of scientific history from the Stone Age to present day, the unprecedented crafting adventure story is about to begin!<br />\r\n<br />\r\n* Based on an adventure sci-fi shounen manga series written by Inagaki Riichirou and illustrated by Boichi.",
"EpCount": 24,
"StudioList": "TMS Entertainment///11|8PAN///6344",
"PastWeek": 0,
"Incomplete": false,
"Ongoing": false,
"Tags": [
"adventure",
"comedy",
"scifi",
"shounen"
],
"Torrents": [
{
"ID": 430074,
"EditionData": {
"EditionTitle": ""
},
"RawDownMultiplier": 1,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/430074/download/somepass",
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (HorribleSubs)",
"Snatched": 108,
"Seeders": 33,
"Leechers": 1,
"Status": 0,
"Size": 16366224176,
"FileCount": 24,
"FileList": [
{
"filename": "[HorribleSubs] Dr. Stone - 01 [720p].mkv",
"size": 477027555
},
{
"filename": "[HorribleSubs] Dr. Stone - 02 [720p].mkv",
"size": 489436551
},
{
"filename": "[HorribleSubs] Dr. Stone - 03 [720p].mkv",
"size": 503786828
},
{
"filename": "[HorribleSubs] Dr. Stone - 04 [720p].mkv",
"size": 442977598
},
{
"filename": "[HorribleSubs] Dr. Stone - 05 [720p].mkv",
"size": 523531555
},
{
"filename": "[HorribleSubs] Dr. Stone - 06 [720p].mkv",
"size": 506742468
},
{
"filename": "[HorribleSubs] Dr. Stone - 07 [720p].mkv",
"size": 746577276
},
{
"filename": "[HorribleSubs] Dr. Stone - 08 [720p].mkv",
"size": 745942485
},
{
"filename": "[HorribleSubs] Dr. Stone - 09 [720p].mkv",
"size": 746035250
},
{
"filename": "[HorribleSubs] Dr. Stone - 10 [720p].mkv",
"size": 746001386
},
{
"filename": "[HorribleSubs] Dr. Stone - 11 [720p].mkv",
"size": 746155088
},
{
"filename": "[HorribleSubs] Dr. Stone - 12 [720p].mkv",
"size": 746560710
},
{
"filename": "[HorribleSubs] Dr. Stone - 13 [720p].mkv",
"size": 745880614
},
{
"filename": "[HorribleSubs] Dr. Stone - 14 [720p].mkv",
"size": 744563919
},
{
"filename": "[HorribleSubs] Dr. Stone - 15 [720p].mkv",
"size": 745303312
},
{
"filename": "[HorribleSubs] Dr. Stone - 16 [720p].mkv",
"size": 746850910
},
{
"filename": "[HorribleSubs] Dr. Stone - 17 [720p].mkv",
"size": 744188496
},
{
"filename": "[HorribleSubs] Dr. Stone - 18 [720p].mkv",
"size": 746212236
},
{
"filename": "[HorribleSubs] Dr. Stone - 19 [720p].mkv",
"size": 744840131
},
{
"filename": "[HorribleSubs] Dr. Stone - 20 [720p].mkv",
"size": 746380081
},
{
"filename": "[HorribleSubs] Dr. Stone - 21 [720p].mkv",
"size": 744975636
},
{
"filename": "[HorribleSubs] Dr. Stone - 22 [720p].mkv",
"size": 746214757
},
{
"filename": "[HorribleSubs] Dr. Stone - 23 [720p].mkv",
"size": 744924693
},
{
"filename": "[HorribleSubs] Dr. Stone - 24 [720p].mkv",
"size": 745114641
}
],
"UploadTime": "2019-12-13 17:02:48"
}
]
},
{
"ID": 41952,
"CategoryName": "Anime",
"FullName": "One Piece - TV Series&nbsp;&nbsp;[2019]",
"GroupName": "TV Series",
"SeriesID": "114",
"SeriesName": "One Piece",
"Artists": null,
"Year": "2019",
"Image": "https://mei.animebytes.tv/cQieN6oZ6Ft.jpg",
"Synonymns": {
"0": "ワンピース",
"2": "One Piece: The Great Gold Pirate"
},
"SynonymnsV2": {
"Japanese": "ワンピース",
"Romaji": "",
"Alternative": "One Piece: The Great Gold Pirate"
},
"Snatched": 100700,
"Comments": 3,
"Links": {
"AniDB": "https://anidb.net/anime/69",
"ANN": "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=836",
"Wikipedia": "https://en.wikipedia.org/wiki/One_Piece",
"MAL": "https://myanimelist.net/anime/21/One_Piece"
},
"Votes": 89,
"AvgVote": 8.8,
"Associations": null,
"Description": "The 20th season of One Piece. This represents episode 892 to current.",
"DescriptionHTML": "The 20th season of One Piece. This represents episode 892 to current.",
"EpCount": 0,
"StudioList": null,
"PastWeek": 10,
"Incomplete": false,
"Ongoing": false,
"Tags": [
"adventure",
"fantasy",
"martial.arts",
"shounen",
"super.power",
"action"
],
"Torrents": [
{
"ID": 1043925,
"EditionData": {
"EditionTitle": "Episode 1059"
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/1043925/download/somepass",
"Property": "Web | MKV | h264 | 720p | AAC 2.0 | Softsubs (SubsPlease) | Episode 1059 | Freeleech",
"Snatched": 125,
"Seeders": 114,
"Leechers": 1,
"Status": 0,
"Size": 743629489,
"FileCount": 1,
"FileList": [
{
"filename": "[SubsPlease] One Piece - 1059 (720p) [B347D9DE].mkv",
"size": 743629489
}
],
"UploadTime": "2023-04-23 02:06:08"
},
{
"ID": 1039046,
"EditionData": {
"EditionTitle": "Episode 1058"
},
"RawDownMultiplier": 0,
"RawUpMultiplier": 1,
"Link": "https://animebytes.tv/torrent/1039046/download/somepass",
"Property": "Web | MKV | h264 | 1080p | AAC 2.0 | Softsubs (SubsPlease) | Episode 1058 | Freeleech",
"Snatched": 290,
"Seeders": 232,
"Leechers": 2,
"Status": 0,
"Size": 1453835224,
"FileCount": 1,
"FileList": [
{
"filename": "[SubsPlease] One Piece - 1058 (1080p) [E4094B4A].mkv",
"size": 1453835224
}
],
"UploadTime": "2023-04-16 02:07:42"
}
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,160 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.AnimeBytesTests
{
[TestFixture]
public class AnimeBytesFixture : CoreTest<AnimeBytes>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition
{
Name = "AnimeBytes",
Settings = new AnimeBytesSettings
{
BaseUrl = "https://animebytes.tv/",
Username = "someuser",
Passkey = "somepass"
}
};
}
[Test]
public async Task should_parse_recent_feed_from_animebytes()
{
var recentFeed = ReadAllText(@"Files/Indexers/AnimeBytes/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000, 5000 } })).Releases;
releases.Should().HaveCount(33);
releases.First().Should().BeOfType<TorrentInfo>();
var firstTorrentInfo = releases.ElementAt(2) as TorrentInfo;
firstTorrentInfo.Title.Should().Be("[SubsPlease] One Piece: The Great Gold Pirate - 1059 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 1059]");
firstTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
firstTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1043925/download/somepass");
firstTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1043925/group");
firstTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1043925/group?nh=0F6BB43603CC07F4C804B9A29139F852");
firstTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
firstTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
firstTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-23 02:06:08", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
firstTorrentInfo.Size.Should().Be(743629489);
firstTorrentInfo.InfoHash.Should().Be(null);
firstTorrentInfo.MagnetUrl.Should().Be(null);
firstTorrentInfo.Peers.Should().Be(1 + 114);
firstTorrentInfo.Seeders.Should().Be(114);
firstTorrentInfo.Files.Should().Be(1);
firstTorrentInfo.MinimumSeedTime.Should().Be(259200);
var secondTorrentInfo = releases.ElementAt(16) as TorrentInfo;
secondTorrentInfo.Title.Should().Be("[GHOST] BLEACH S03 [Blu-ray][MKV][h265 10-bit][1080p][AC3 2.0][Dual Audio][Softsubs (GHOST)]");
secondTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
secondTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1031203/download/somepass");
secondTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1031203/group");
secondTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1031203/group?nh=F7C73EF631FE269D3A7F10BD12EC99A1");
secondTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
secondTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
secondTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-03 03:14:35", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
secondTorrentInfo.Size.Should().Be(24498538059);
secondTorrentInfo.InfoHash.Should().Be(null);
secondTorrentInfo.MagnetUrl.Should().Be(null);
secondTorrentInfo.Peers.Should().Be(2 + 12);
secondTorrentInfo.Seeders.Should().Be(12);
secondTorrentInfo.Files.Should().Be(22);
secondTorrentInfo.MinimumSeedTime.Should().Be(655200);
var thirdTorrentInfo = releases.ElementAt(18) as TorrentInfo;
thirdTorrentInfo.Title.Should().Be("[Polarwindz] Cowboy Bebop: Tengoku no Tobira 2001 [Blu-ray][MKV][h265 10-bit][1080p][Opus 5.1][Softsubs (Polarwindz)]");
thirdTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
thirdTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/959397/download/somepass");
thirdTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/959397/group");
thirdTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/959397/group?nh=D63895DA87A25239C11F9823F46000E1");
thirdTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
thirdTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
thirdTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-02 05:00:43", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
thirdTorrentInfo.Size.Should().Be(13090646841);
thirdTorrentInfo.InfoHash.Should().Be(null);
thirdTorrentInfo.MagnetUrl.Should().Be(null);
thirdTorrentInfo.Peers.Should().Be(1 + 5);
thirdTorrentInfo.Seeders.Should().Be(5);
thirdTorrentInfo.Files.Should().Be(1);
thirdTorrentInfo.MinimumSeedTime.Should().Be(475200);
var fourthTorrentInfo = releases.ElementAt(3) as TorrentInfo;
fourthTorrentInfo.Title.Should().Be("[SubsPlease] Dr. STONE: NEW WORLD S03E03 - 03 [Web][MKV][h264][720p][AAC 2.0][Softsubs (SubsPlease)][Episode 3]");
fourthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
fourthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/1041495/download/somepass");
fourthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/1041495/group");
fourthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/1041495/group?nh=8B78B0DD3BCC6068BFCD927E4AC674F6");
fourthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
fourthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
fourthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2023-04-20 14:32:29", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
fourthTorrentInfo.Size.Should().Be(748209543);
fourthTorrentInfo.InfoHash.Should().Be(null);
fourthTorrentInfo.MagnetUrl.Should().Be(null);
fourthTorrentInfo.Peers.Should().Be(3 + 137);
fourthTorrentInfo.Seeders.Should().Be(137);
fourthTorrentInfo.Files.Should().Be(1);
fourthTorrentInfo.MinimumSeedTime.Should().Be(259200);
var fifthTorrentInfo = releases.ElementAt(23) as TorrentInfo;
fifthTorrentInfo.Title.Should().Be("[-ZR-] Dr. STONE: STONE WARS S02 [Web][MKV][h264][1080p][AAC 2.0][Dual Audio][Softsubs (-ZR-)]");
fifthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
fifthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/944509/download/somepass");
fifthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/944509/group");
fifthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/944509/group?nh=FDCAA1EAB36D7C802F1E4B13DAE5EED7");
fifthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
fifthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
fifthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-06-03 20:30:00", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
fifthTorrentInfo.Size.Should().Be(16611719364);
fifthTorrentInfo.InfoHash.Should().Be(null);
fifthTorrentInfo.MagnetUrl.Should().Be(null);
fifthTorrentInfo.Peers.Should().Be(1 + 31);
fifthTorrentInfo.Seeders.Should().Be(31);
fifthTorrentInfo.Files.Should().Be(11);
fifthTorrentInfo.MinimumSeedTime.Should().Be(529200);
var sixthTorrentInfo = releases.ElementAt(31) as TorrentInfo;
sixthTorrentInfo.Title.Should().Be("[HorribleSubs] Dr. STONE S01 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]");
sixthTorrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
sixthTorrentInfo.DownloadUrl.Should().Be("https://animebytes.tv/torrent/430074/download/somepass");
sixthTorrentInfo.InfoUrl.Should().Be("https://animebytes.tv/torrent/430074/group");
sixthTorrentInfo.Guid.Should().Be("https://animebytes.tv/torrent/430074/group?nh=32279E138015D8718B2B4B49AEF64574");
sixthTorrentInfo.CommentUrl.Should().BeNullOrEmpty();
sixthTorrentInfo.Indexer.Should().Be(Subject.Definition.Name);
sixthTorrentInfo.PublishDate.Should().Be(DateTime.Parse("2019-12-13 17:02:48", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal));
sixthTorrentInfo.Size.Should().Be(16366224176);
sixthTorrentInfo.InfoHash.Should().Be(null);
sixthTorrentInfo.MagnetUrl.Should().Be(null);
sixthTorrentInfo.Peers.Should().Be(1 + 33);
sixthTorrentInfo.Seeders.Should().Be(33);
sixthTorrentInfo.Files.Should().Be(24);
sixthTorrentInfo.MinimumSeedTime.Should().Be(529200);
}
}
}

View File

@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
Subject.Definition = new IndexerDefinition
{
Name = "AvistaZ",
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
};
}
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(100);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://avistaz.to/torrent/187240-japan-sinks-people-of-hope-2021-s01e05-720p-nf-web-dl-ddp20-x264-seikel");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 22:26:21"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-11-14 21:26:21"));
torrentInfo.Size.Should().Be(935127615);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
Subject.Definition = new IndexerDefinition
{
Name = "ExoticaZ",
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
};
}
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(100);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://exoticaz.to/torrent/64040-ssis-419-my-first-experience-is-yua-mikami-from-the-day-i-lost-my-virginity-i-was-devoted-to-sex");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 16:04:50"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-06-11 15:04:50"));
torrentInfo.Size.Should().Be(7085405541);
torrentInfo.InfoHash.Should().Be("asdjfiasdf54asd7f4a2sdf544asdf");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -22,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
Subject.Definition = new IndexerDefinition
{
Name = "PrivateHD",
Settings = new AvistazSettings() { Username = "someuser", Password = "somepass", Pid = "somepid" }
Settings = new AvistazSettings { Username = "someuser", Password = "somepass", Pid = "somepid" }
};
}
@@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new int[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new MovieSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(100);
releases.First().Should().BeOfType<TorrentInfo>();
@@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.IndexerTests.AvistazTests
torrentInfo.InfoUrl.Should().Be("https://privatehd.to/torrent/78506-godzilla-2014-2160p-uhd-bluray-remux-hdr-hevc-atmos-triton");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 05:24:49"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2021-03-21 04:24:49"));
torrentInfo.Size.Should().Be(69914591044);
torrentInfo.InfoHash.Should().Be("a879261d4e6e792402f92401141a21de70d51bf2");
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -0,0 +1,296 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DryIoc;
using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.BroadcastheNet;
using NzbDrone.Core.Indexers.Definitions.HDBits;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
{
public class BroadcastheNetRequestGeneratorFixture : CoreTest<BroadcastheNetRequestGenerator>
{
[SetUp]
public void Setup()
{
Subject.Settings = new BroadcastheNetSettings
{
BaseUrl = "https://api.broadcasthe.net/",
ApiKey = "abc"
};
Subject.Capabilities = new IndexerCapabilities
{
LimitsDefault = 100,
LimitsMax = 1000,
TvSearchParams = new List<TvSearchParam>
{
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.TvdbId, TvSearchParam.RId
}
};
}
[Test]
public void should_have_empty_parameters_if_rss_search()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id }
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().BeNull();
query.Tvrage.Should().BeNull();
query.Search.Should().BeNullOrWhiteSpace();
query.Category.Should().BeNullOrWhiteSpace();
query.Name.Should().BeNullOrWhiteSpace();
}
[Test]
public void should_search_by_tvdbid_season_if_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
TvdbId = 371980,
Season = 1
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(2);
var firstPage = results.GetAllTiers().First().First();
var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest);
firstQuery.Tvdb.Should().Be("371980");
firstQuery.Tvrage.Should().BeNull();
firstQuery.Search.Should().BeNull();
firstQuery.Category.Should().Be("Season");
firstQuery.Name.Should().Be("Season 1%");
var secondPage = results.GetAllTiers().Skip(1).First().First();
var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest);
secondQuery.Tvdb.Should().Be("371980");
secondQuery.Tvrage.Should().BeNull();
secondQuery.Search.Should().BeNull();
secondQuery.Category.Should().Be("Episode");
secondQuery.Name.Should().Be("S01E%");
}
[Test]
public void should_search_by_tvdbid_season_episode_if_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
TvdbId = 371980,
Season = 1,
Episode = "3"
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().Be("371980");
query.Tvrage.Should().BeNull();
query.Search.Should().BeNull();
query.Category.Should().Be("Episode");
query.Name.Should().Be("S01E03");
}
[Test]
public void should_search_by_tvdbid_daily_episode_if_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
TvdbId = 289574,
Season = 2023,
Episode = "01/03"
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().Be("289574");
query.Tvrage.Should().BeNull();
query.Search.Should().BeNull();
query.Category.Should().Be("Episode");
query.Name.Should().Be("2023.01.03");
}
[Test]
public void should_prefer_search_by_tvdbid_if_rid_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
TvdbId = 371980,
RId = 12345
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().Be("371980");
query.Tvrage.Should().BeNull();
query.Search.Should().BeNull();
query.Category.Should().BeNull();
query.Name.Should().BeNull();
}
[Test]
public void should_search_by_term_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
SearchTerm = "Malcolm in the Middle"
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().BeNull();
query.Tvrage.Should().BeNull();
query.Search.Should().Be("Malcolm%in%the%Middle");
query.Category.Should().BeNull();
query.Name.Should().BeNull();
}
[Test]
public void should_search_by_term_season_if_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
SearchTerm = "Malcolm in the Middle",
Season = 2
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(2);
var firstPage = results.GetAllTiers().First().First();
var firstQuery = ParseTorrentQueryFromRequest(firstPage.HttpRequest);
firstQuery.Tvdb.Should().BeNull();
firstQuery.Tvrage.Should().BeNull();
firstQuery.Search.Should().Be("Malcolm%in%the%Middle");
firstQuery.Category.Should().Be("Season");
firstQuery.Name.Should().Be("Season 2%");
var secondPage = results.GetAllTiers().Skip(1).First().First();
var secondQuery = ParseTorrentQueryFromRequest(secondPage.HttpRequest);
secondQuery.Tvdb.Should().BeNull();
secondQuery.Tvrage.Should().BeNull();
secondQuery.Search.Should().Be("Malcolm%in%the%Middle");
secondQuery.Category.Should().Be("Episode");
secondQuery.Name.Should().Be("S02E%");
}
[Test]
public void should_search_by_term_season_episode_if_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
SearchTerm = "Malcolm in the Middle",
Season = 2,
Episode = "3"
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().BeNull();
query.Tvrage.Should().BeNull();
query.Search.Should().Be("Malcolm%in%the%Middle");
query.Category.Should().Be("Episode");
query.Name.Should().Be("S02E03");
}
[Test]
public void should_search_by_term_daily_episode_if_supported()
{
var tvSearchCriteria = new TvSearchCriteria
{
Categories = new[] { NewznabStandardCategory.TV.Id, NewznabStandardCategory.TVHD.Id },
SearchTerm = "The Late Show with Stephen Colbert",
Season = 2023,
Episode = "01/03"
};
var results = Subject.GetSearchRequests(tvSearchCriteria);
results.Tiers.Should().Be(1);
results.GetAllTiers().Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
var query = ParseTorrentQueryFromRequest(page.HttpRequest);
query.Tvdb.Should().BeNull();
query.Tvrage.Should().BeNull();
query.Search.Should().Be("The%Late%Show%with%Stephen%Colbert");
query.Category.Should().Be("Episode");
query.Name.Should().Be("2023.01.03");
}
private static BroadcastheNetTorrentQuery ParseTorrentQueryFromRequest(HttpRequest httpRequest)
{
var encoding = HttpHeader.GetEncodingFromContentType(httpRequest.Headers.ContentType);
var body = encoding.GetString(httpRequest.ContentData);
var rpcBody = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
return JsonConvert.DeserializeObject<BroadcastheNetTorrentQuery>(((JArray)rpcBody["params"])[1].ToJson());
}
}
}

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.CardigannTests

View File

@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
torrentInfo.InfoUrl.Should().Be("https://filelist.io/details.php?id=665873");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 20:20:19"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2020-01-25 19:20:19"));
torrentInfo.Size.Should().Be(8300512414);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition()
Subject.Definition = new IndexerDefinition
{
Name = "Orpheus",
Settings = new OrpheusSettings { Apikey = "somekey" }
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases;
releases.Should().HaveCount(65);
releases.First().Should().BeOfType<GazelleInfo>();
@@ -56,6 +56,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OrpheusTests
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(0);
torrentInfo.Seeders.Should().Be(0);
torrentInfo.Files.Should().Be(18);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);

View File

@@ -0,0 +1,69 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Definitions;
using NzbDrone.Core.Indexers.Definitions.Gazelle;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerTests.RedactedTests
{
[TestFixture]
public class RedactedFixture : CoreTest<Redacted>
{
[SetUp]
public void Setup()
{
Subject.Definition = new IndexerDefinition
{
Name = "Redacted",
Settings = new RedactedSettings { Apikey = "somekey" }
};
}
[Test]
public async Task should_parse_recent_feed_from_Redacted()
{
var recentFeed = ReadAllText(@"Files/Indexers/Redacted/recentfeed.json");
Mocker.GetMock<IIndexerHttpClient>()
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 3000 } })).Releases;
releases.Should().HaveCount(39);
releases.First().Should().BeOfType<GazelleInfo>();
var torrentInfo = releases.First() as GazelleInfo;
torrentInfo.Title.Should().Be("Red Hot Chili Peppers - Californication [1999] [Album] [US / Reissue 2020] [FLAC 24bit Lossless] [Vinyl]");
torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
torrentInfo.DownloadUrl.Should().Be("https://redacted.ch/ajax.php?action=download&id=3892313&usetoken=0");
torrentInfo.InfoUrl.Should().Be("https://redacted.ch/torrents.php?id=16720&torrentid=3892313");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-17 08:02:35"));
torrentInfo.Size.Should().Be(1247137236);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);
torrentInfo.Peers.Should().Be(4);
torrentInfo.Seeders.Should().Be(4);
torrentInfo.Files.Should().Be(23);
torrentInfo.ImdbId.Should().Be(0);
torrentInfo.TmdbId.Should().Be(0);
torrentInfo.TvdbId.Should().Be(0);
torrentInfo.Languages.Should().HaveCount(0);
torrentInfo.Subs.Should().HaveCount(0);
torrentInfo.DownloadVolumeFactor.Should().Be(1);
torrentInfo.UploadVolumeFactor.Should().Be(1);
}
}
}

View File

@@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
Subject.Definition = new IndexerDefinition()
{
Name = "SecretCinema",
Settings = new GazelleSettings() { Username = "somekey", Password = "somekey" }
Settings = new GazelleSettings { Username = "somekey", Password = "somekey" }
};
}
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
.Setup(o => o.ExecuteProxiedAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed)));
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases;
var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases;
releases.Should().HaveCount(3);
releases.First().Should().BeOfType<GazelleInfo>();
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Test.IndexerTests.SecretCinemaTests
torrentInfo.InfoUrl.Should().Be("https://secret-cinema.pw/torrents.php?id=2497&torrentid=45068");
torrentInfo.CommentUrl.Should().BeNullOrEmpty();
torrentInfo.Indexer.Should().Be(Subject.Definition.Name);
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 19:37:29"));
torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-12-15 17:37:29"));
torrentInfo.Size.Should().Be(57473058680);
torrentInfo.InfoHash.Should().Be(null);
torrentInfo.MagnetUrl.Should().Be(null);

View File

@@ -210,11 +210,11 @@ namespace NzbDrone.Core.Applications
if (intersectingTags.Any())
{
_logger.Debug("Application {0} and indexer {1} [{2}] have {3} intersecting tags.", app.Name, indexer.Name, indexer.Id, intersectingTags.Length);
_logger.Debug("Application {0} and indexer {1} [{2}] have {3} intersecting (matching) tags.", app.Name, indexer.Name, indexer.Id, intersectingTags.Length);
return true;
}
_logger.Info("Application {0} does not have any intersecting tags with {1} [{2}]. Indexer will not be handled.", app.Name, indexer.Name, indexer.Id);
_logger.Debug("Application {0} does not have any intersecting (matching) tags with {1} [{2}]. Indexer will neither be synced to nor removed from the application.", app.Name, indexer.Name, indexer.Id);
return false;
}

View File

@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.LazyLibrarian
public class LazyLibrarianSettings : IApplicationSettings
{
private static readonly LazyLibrarianSettingsValidator Validator = new LazyLibrarianSettingsValidator();
private static readonly LazyLibrarianSettingsValidator Validator = new ();
public LazyLibrarianSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:5299";
SyncCategories = new[]
{
NewznabStandardCategory.AudioAudiobook.Id,

View File

@@ -18,10 +18,12 @@ namespace NzbDrone.Core.Applications.Lidarr
public class LidarrSettings : IApplicationSettings
{
private static readonly LidarrSettingsValidator Validator = new LidarrSettingsValidator();
private static readonly LidarrSettingsValidator Validator = new ();
public LidarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8686";
SyncCategories = new[] { 3000, 3010, 3030, 3040, 3050, 3060 };
}

View File

@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Lidarr
public class LidarrV1Proxy : ILidarrV1Proxy
{
private const string AppApiRoute = "/api/v1";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Lidarr
public LidarrStatus GetStatus(LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/system/status", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
return Execute<LidarrStatus>(request);
}
public List<LidarrIndexer> GetIndexers(LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
return Execute<List<LidarrIndexer>>(request);
}
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Lidarr
{
try
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
return Execute<LidarrIndexer>(request);
}
catch (HttpException ex)
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Lidarr
public void RemoveIndexer(int indexerId, LidarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Delete);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer/schema", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
return Execute<List<LidarrIndexer>>(request);
}
public LidarrIndexer AddIndexer(LidarrIndexer indexer, LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
request.SetContent(indexer.ToJson());
return Execute<LidarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexer.Id}", HttpMethod.Put);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<LidarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Lidarr
return null;
}
private LidarrIndexer ExecuteIndexerRequest(HttpRequest request)
{
try
{
return Execute<LidarrIndexer>(request);
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Error(ex, "API Key is invalid");
break;
case HttpStatusCode.BadRequest:
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
break;
}
_logger.Error(ex, "Invalid Request");
break;
case HttpStatusCode.SeeOther:
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
break;
case HttpStatusCode.NotFound:
_logger.Error(ex, "Remote indexer not found");
break;
default:
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
throw;
}
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to add or update indexer");
throw;
}
return null;
}
private HttpRequest BuildRequest(LidarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');

View File

@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Mylar
public class MylarSettings : IApplicationSettings
{
private static readonly MylarSettingsValidator Validator = new MylarSettingsValidator();
private static readonly MylarSettingsValidator Validator = new ();
public MylarSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8090";
SyncCategories = new[] { NewznabStandardCategory.BooksComics.Id };
}

View File

@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Radarr
public class RadarrSettings : IApplicationSettings
{
private static readonly RadarrSettingsValidator Validator = new RadarrSettingsValidator();
private static readonly RadarrSettingsValidator Validator = new ();
public RadarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:7878";
SyncCategories = new[] { 2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080 };
}

View File

@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Radarr
public class RadarrV3Proxy : IRadarrV3Proxy
{
private const string AppApiRoute = "/api/v3";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Radarr
public RadarrStatus GetStatus(RadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
return Execute<RadarrStatus>(request);
}
public List<RadarrIndexer> GetIndexers(RadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
return Execute<List<RadarrIndexer>>(request);
}
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Radarr
{
try
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
return Execute<RadarrIndexer>(request);
}
catch (HttpException ex)
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Radarr
public void RemoveIndexer(int indexerId, RadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<RadarrIndexer> GetIndexerSchema(RadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
return Execute<List<RadarrIndexer>>(request);
}
public RadarrIndexer AddIndexer(RadarrIndexer indexer, RadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
request.SetContent(indexer.ToJson());
return Execute<RadarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<RadarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Radarr
return null;
}
private RadarrIndexer ExecuteIndexerRequest(HttpRequest request)
{
try
{
return Execute<RadarrIndexer>(request);
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Error(ex, "API Key is invalid");
break;
case HttpStatusCode.BadRequest:
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
break;
}
_logger.Error(ex, "Invalid Request");
break;
case HttpStatusCode.SeeOther:
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
break;
case HttpStatusCode.NotFound:
_logger.Error(ex, "Remote indexer not found");
break;
default:
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
throw;
}
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to add or update indexer");
throw;
}
return null;
}
private HttpRequest BuildRequest(RadarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');

View File

@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Readarr
public class ReadarrSettings : IApplicationSettings
{
private static readonly ReadarrSettingsValidator Validator = new ReadarrSettingsValidator();
private static readonly ReadarrSettingsValidator Validator = new ();
public ReadarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8787";
SyncCategories = new[] { 3030, 7000, 7010, 7020, 7030, 7040, 7050, 7060 };
}

View File

@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Readarr
public class ReadarrV1Proxy : IReadarrV1Proxy
{
private const string AppApiRoute = "/api/v1";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Readarr
public ReadarrStatus GetStatus(ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/system/status", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
return Execute<ReadarrStatus>(request);
}
public List<ReadarrIndexer> GetIndexers(ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
return Execute<List<ReadarrIndexer>>(request);
}
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Readarr
{
try
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
return Execute<ReadarrIndexer>(request);
}
catch (HttpException ex)
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Readarr
public void RemoveIndexer(int indexerId, ReadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.Delete);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer/schema", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
return Execute<List<ReadarrIndexer>>(request);
}
public ReadarrIndexer AddIndexer(ReadarrIndexer indexer, ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
request.SetContent(indexer.ToJson());
return Execute<ReadarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexer.Id}", HttpMethod.Put);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<ReadarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Readarr
return null;
}
private ReadarrIndexer ExecuteIndexerRequest(HttpRequest request)
{
try
{
return Execute<ReadarrIndexer>(request);
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Error(ex, "API Key is invalid");
break;
case HttpStatusCode.BadRequest:
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
break;
}
_logger.Error(ex, "Invalid Request");
break;
case HttpStatusCode.SeeOther:
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
break;
case HttpStatusCode.NotFound:
_logger.Error(ex, "Remote indexer not found");
break;
default:
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
throw;
}
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to add or update indexer");
throw;
}
return null;
}
private HttpRequest BuildRequest(ReadarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');

View File

@@ -18,10 +18,12 @@ namespace NzbDrone.Core.Applications.Sonarr
public class SonarrSettings : IApplicationSettings
{
private static readonly SonarrSettingsValidator Validator = new SonarrSettingsValidator();
private static readonly SonarrSettingsValidator Validator = new ();
public SonarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8989";
SyncCategories = new[] { 5000, 5010, 5020, 5030, 5040, 5045, 5050 };
AnimeSyncCategories = new[] { 5070 };
}

View File

@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Sonarr
public class SonarrV3Proxy : ISonarrV3Proxy
{
private const string AppApiRoute = "/api/v3";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Sonarr
public SonarrStatus GetStatus(SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
return Execute<SonarrStatus>(request);
}
public List<SonarrIndexer> GetIndexers(SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
return Execute<List<SonarrIndexer>>(request);
}
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Sonarr
{
try
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
return Execute<SonarrIndexer>(request);
}
catch (HttpException ex)
@@ -64,37 +66,37 @@ namespace NzbDrone.Core.Applications.Sonarr
public void RemoveIndexer(int indexerId, SonarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
return Execute<List<SonarrIndexer>>(request);
}
public SonarrIndexer AddIndexer(SonarrIndexer indexer, SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
request.SetContent(indexer.ToJson());
return Execute<SonarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<SonarrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
@@ -140,6 +142,53 @@ namespace NzbDrone.Core.Applications.Sonarr
return null;
}
private SonarrIndexer ExecuteIndexerRequest(HttpRequest request)
{
try
{
return Execute<SonarrIndexer>(request);
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Error(ex, "API Key is invalid");
break;
case HttpStatusCode.BadRequest:
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
break;
}
_logger.Error(ex, "Invalid Request");
break;
case HttpStatusCode.SeeOther:
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
break;
case HttpStatusCode.NotFound:
_logger.Error(ex, "Remote indexer not found");
break;
default:
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
throw;
}
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to add or update indexer");
throw;
}
return null;
}
private HttpRequest BuildRequest(SonarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');

View File

@@ -19,10 +19,12 @@ namespace NzbDrone.Core.Applications.Whisparr
public class WhisparrSettings : IApplicationSettings
{
private static readonly WhisparrSettingsValidator Validator = new WhisparrSettingsValidator();
private static readonly WhisparrSettingsValidator Validator = new ();
public WhisparrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:6969";
SyncCategories = new[] { 6000, 6010, 6020, 6030, 6040, 6045, 6050, 6070, 6080, 6090 };
}

View File

@@ -23,6 +23,8 @@ namespace NzbDrone.Core.Applications.Whisparr
public class WhisparrV3Proxy : IWhisparrV3Proxy
{
private const string AppApiRoute = "/api/v3";
private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer";
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
@@ -34,13 +36,13 @@ namespace NzbDrone.Core.Applications.Whisparr
public WhisparrStatus GetStatus(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get);
return Execute<WhisparrStatus>(request);
}
public List<WhisparrIndexer> GetIndexers(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
return Execute<List<WhisparrIndexer>>(request);
}
@@ -48,7 +50,7 @@ namespace NzbDrone.Core.Applications.Whisparr
{
try
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
return Execute<WhisparrIndexer>(request);
}
catch (HttpException ex)
@@ -64,19 +66,19 @@ namespace NzbDrone.Core.Applications.Whisparr
public void RemoveIndexer(int indexerId, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
return Execute<List<WhisparrIndexer>>(request);
}
public WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
request.SetContent(indexer.ToJson());
@@ -85,16 +87,16 @@ namespace NzbDrone.Core.Applications.Whisparr
public WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<WhisparrIndexer>(request);
return ExecuteIndexerRequest(request);
}
public ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
@@ -134,6 +136,53 @@ namespace NzbDrone.Core.Applications.Whisparr
return null;
}
private WhisparrIndexer ExecuteIndexerRequest(HttpRequest request)
{
try
{
return Execute<WhisparrIndexer>(request);
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Error(ex, "API Key is invalid");
break;
case HttpStatusCode.BadRequest:
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Error(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
break;
}
_logger.Error(ex, "Invalid Request");
break;
case HttpStatusCode.SeeOther:
_logger.Error(ex, "App returned redirect and is invalid. Check App URL");
break;
case HttpStatusCode.NotFound:
_logger.Error(ex, "Remote indexer not found");
break;
default:
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
throw;
}
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to add or update indexer");
throw;
}
return null;
}
private HttpRequest BuildRequest(WhisparrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');

View File

@@ -69,7 +69,8 @@ namespace NzbDrone.Core.Backup
_diskProvider.EnsureFolder(_backupTempFolder);
_diskProvider.EnsureFolder(GetBackupFolder(backupType));
var backupFilename = string.Format("prowlarr_backup_v{0}_{1:yyyy.MM.dd_HH.mm.ss}.zip", BuildInfo.Version, DateTime.Now);
var dateNow = DateTime.Now;
var backupFilename = $"prowlarr_backup_v{BuildInfo.Version}_{dateNow:yyyy.MM.dd_HH.mm.ss}.zip";
var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename);
Cleanup();
@@ -81,7 +82,7 @@ namespace NzbDrone.Core.Backup
BackupConfigFile();
BackupDatabase();
CreateVersionInfo();
CreateVersionInfo(dateNow);
_logger.ProgressDebug("Creating backup zip");
@@ -208,11 +209,15 @@ namespace NzbDrone.Core.Backup
_diskTransferService.TransferFile(configFile, tempConfigFile, TransferMode.Copy);
}
private void CreateVersionInfo()
private void CreateVersionInfo(DateTime dateNow)
{
var builder = new StringBuilder();
var tempFile = Path.Combine(_backupTempFolder, "INFO");
builder.AppendLine(BuildInfo.Version.ToString());
var builder = new StringBuilder();
builder.AppendLine($"v{BuildInfo.Version}");
builder.AppendLine($"{dateNow:yyyy-MM-dd HH:mm:ss}");
_diskProvider.WriteAllText(tempFile, builder.ToString());
}
private void CleanupOldBackups(BackupType backupType)

View File

@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Configuration
{
Dictionary<string, object> GetConfigDictionary();
void SaveConfigDictionary(Dictionary<string, object> configValues);
void EnsureDefaultConfigFile();
string BindAddress { get; }
int Port { get; }
@@ -258,7 +259,7 @@ namespace NzbDrone.Core.Configuration
public T GetValueEnum<T>(string key, T defaultValue, bool persist = true)
{
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue), persist);
return (T)Enum.Parse(typeof(T), GetValue(key, defaultValue, persist), true);
}
public string GetValue(string key, object defaultValue, bool persist = true)
@@ -317,7 +318,7 @@ namespace NzbDrone.Core.Configuration
SetValue(key, value.ToString().ToLower());
}
private void EnsureDefaultConfigFile()
public void EnsureDefaultConfigFile()
{
if (!File.Exists(_configFile))
{

View File

@@ -103,7 +103,7 @@ namespace NzbDrone.Core.Datastore
{
if (!ids.Any())
{
return new List<TModel>();
return Array.Empty<TModel>();
}
var result = Query(x => ids.Contains(x.Id));

View File

@@ -1,5 +1,7 @@
using System;
using System.Data;
using System.Data.Common;
using System.Data.SQLite;
using System.Text.RegularExpressions;
using Dapper;
using NLog;
@@ -38,17 +40,9 @@ namespace NzbDrone.Core.Datastore
{
get
{
using (var db = _datamapperFactory())
{
if (db.ConnectionString.Contains(".db"))
{
return DatabaseType.SQLite;
}
else
{
return DatabaseType.PostgreSQL;
}
}
using var db = _datamapperFactory();
return db is SQLiteConnection ? DatabaseType.SQLite : DatabaseType.PostgreSQL;
}
}
@@ -56,24 +50,11 @@ namespace NzbDrone.Core.Datastore
{
get
{
using (var db = _datamapperFactory())
{
string version;
using var db = _datamapperFactory();
var dbConnection = db as DbConnection;
var version = Regex.Replace(dbConnection.ServerVersion, @"\(.*?\)", "");
try
{
version = db.QueryFirstOrDefault<string>("SHOW server_version");
//Postgres can return extra info about operating system on version call, ignore this
version = Regex.Replace(version, @"\(.*?\)", "");
}
catch
{
version = db.QueryFirstOrDefault<string>("SELECT sqlite_version()");
}
return new Version(version);
}
return new Version(version);
}
}

View File

@@ -0,0 +1,49 @@
using System.Data;
using Dapper;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(031)]
public class apprise_server_url : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToServerUrl);
}
private void MigrateToServerUrl(IDbConnection conn, IDbTransaction tran)
{
using var selectCommand = conn.CreateCommand();
selectCommand.Transaction = tran;
selectCommand.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Apprise'";
using var reader = selectCommand.ExecuteReader();
while (reader.Read())
{
var id = reader.GetInt32(0);
var settings = reader.GetString(1);
if (!string.IsNullOrWhiteSpace(settings))
{
var jsonObject = Json.Deserialize<JObject>(settings);
if (jsonObject.ContainsKey("baseUrl"))
{
jsonObject.Add("serverUrl", jsonObject.Value<string>("baseUrl"));
jsonObject.Remove("baseUrl");
}
settings = jsonObject.ToJson();
}
var parameters = new { Settings = settings, Id = id };
conn.Execute("UPDATE Notifications SET Settings = @Settings WHERE Id = @Id", parameters, transaction: tran);
}
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using NLog;
@@ -64,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
catch (DownloadClientException e)
{
_logger.Error(e);
return new List<DownloadStationTask>();
return Array.Empty<DownloadStationTask>();
}
}

View File

@@ -45,6 +45,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return true;
}
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new DownloadClientException("Failed to connect to qBittorrent. Check your settings and qBittorrent configuration.", new HttpException(response));
}
if (response.HasHttpError)
{
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));

View File

@@ -16,12 +16,12 @@ namespace NzbDrone.Core.Download.Clients.rTorrent
public RTorrentDirectoryValidator(PathExistsValidator pathExistsValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator)
{
RuleFor(c => c.Directory).Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath()
.SetValidator(mappedNetworkDriveValidator)
.SetValidator(pathExistsValidator)
.When(c => c.Directory.IsNotNullOrWhiteSpace())
.When(c => c.Host == "localhost" || c.Host == "127.0.0.1");
RuleFor(c => c.Directory).Cascade(CascadeMode.Stop)
.IsValidPath()
.SetValidator(mappedNetworkDriveValidator)
.SetValidator(pathExistsValidator)
.When(c => c.Directory.IsNotNullOrWhiteSpace())
.When(c => c.Host == "localhost" || c.Host == "127.0.0.1");
}
}
}

View File

@@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
#if !LIBRARY
using NzbDrone.Common.EnsureThat;
#endif
namespace NzbDrone.Core
{
@@ -12,9 +10,8 @@ namespace NzbDrone.Core
{
public static string WithDefault(this string actual, object defaultValue)
{
#if !LIBRARY
Ensure.That(defaultValue, () => defaultValue).IsNotNull();
#endif
if (string.IsNullOrWhiteSpace(actual))
{
return defaultValue.ToString();

View File

@@ -1,7 +1,6 @@
using System.Linq;
using NLog;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;

View File

@@ -1,7 +1,7 @@
using System.Linq;
using NLog;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
using NzbDrone.Core.IndexerVersions;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;

View File

@@ -68,12 +68,9 @@ namespace NzbDrone.Core.HealthCheck.Checks
}
}
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14))
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14) && _checkUpdateService.AvailableUpdate() != null)
{
if (_checkUpdateService.AvailableUpdate() != null)
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, "New update is available");
}
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("UpdateAvailable"), "#new-update-is-available");
}
return new HealthCheck(GetType());

View File

@@ -31,9 +31,7 @@ namespace NzbDrone.Core.History
public History MostRecentForDownloadId(string downloadId)
{
return FindByDownloadId(downloadId)
.OrderByDescending(h => h.Date)
.FirstOrDefault();
return FindByDownloadId(downloadId).MaxBy(h => h.Date);
}
public List<History> FindByDownloadId(string downloadId)
@@ -75,9 +73,7 @@ namespace NzbDrone.Core.History
public History MostRecentForIndexer(int indexerId)
{
return Query(x => x.IndexerId == indexerId)
.OrderByDescending(h => h.Date)
.FirstOrDefault();
return Query(x => x.IndexerId == indexerId).MaxBy(h => h.Date);
}
public List<History> Between(DateTime start, DateTime end)

View File

@@ -118,12 +118,14 @@ namespace NzbDrone.Core.History
public void Handle(IndexerQueryEvent message)
{
var response = message.QueryResult.Response;
var history = new History
{
Date = DateTime.UtcNow,
IndexerId = message.IndexerId,
EventType = message.Query.IsRssSearch ? HistoryEventType.IndexerRss : HistoryEventType.IndexerQuery,
Successful = message.QueryResult.Response?.StatusCode == HttpStatusCode.OK
Successful = response?.StatusCode == HttpStatusCode.OK || (response is { Request: { SuppressHttpError: true, SuppressHttpErrorStatusCodes: not null } } && response.Request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode))
};
if (message.Query is MovieSearchCriteria)
@@ -168,7 +170,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Genre", ((BookSearchCriteria)message.Query).Genre ?? string.Empty);
}
history.Data.Add("ElapsedTime", message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty);
history.Data.Add("ElapsedTime", message.QueryResult.Cached ? "0" : message.QueryResult.Response?.ElapsedTime.ToString() ?? string.Empty);
history.Data.Add("Query", message.Query.SearchTerm ?? string.Empty);
history.Data.Add("QueryType", message.Query.SearchType ?? string.Empty);
history.Data.Add("Categories", string.Join(",", message.Query.Categories) ?? string.Empty);
@@ -176,6 +178,7 @@ namespace NzbDrone.Core.History
history.Data.Add("Host", message.Query.Host ?? string.Empty);
history.Data.Add("QueryResults", message.QueryResult.Releases?.Count.ToString() ?? string.Empty);
history.Data.Add("Url", message.QueryResult.Response?.Request.Url.FullUri ?? string.Empty);
history.Data.Add("Cached", message.QueryResult.Cached ? "1" : "0");
_historyRepository.Insert(history);
}

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.IndexerSearch
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
RegexOptions.Compiled);
public List<ReleaseInfo> Releases { get; set; }
public IList<ReleaseInfo> Releases { get; set; }
private static string RemoveInvalidXMLChars(string text)
{

View File

@@ -8,6 +8,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Events;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.IndexerSearch
@@ -56,7 +57,9 @@ namespace NzbDrone.Core.IndexerSearch
{
var searchSpec = Get<MovieSearchCriteria>(request, indexerIds, interactiveSearch);
searchSpec.ImdbId = request.imdbid;
var imdbId = ParseUtil.GetImdbID(request.imdbid);
searchSpec.ImdbId = imdbId.HasValue ? imdbId.Value.ToString() : null;
searchSpec.TmdbId = request.tmdbid;
searchSpec.TraktId = request.traktid;
searchSpec.DoubanId = request.doubanid;
@@ -84,10 +87,12 @@ namespace NzbDrone.Core.IndexerSearch
{
var searchSpec = Get<TvSearchCriteria>(request, indexerIds, interactiveSearch);
var imdbId = ParseUtil.GetImdbID(request.imdbid);
searchSpec.ImdbId = imdbId.HasValue ? imdbId.Value.ToString() : null;
searchSpec.Season = request.season;
searchSpec.Episode = request.ep;
searchSpec.TvdbId = request.tvdbid;
searchSpec.ImdbId = request.imdbid;
searchSpec.TraktId = request.traktid;
searchSpec.TmdbId = request.tmdbid;
searchSpec.DoubanId = request.doubanid;
@@ -136,7 +141,7 @@ namespace NzbDrone.Core.IndexerSearch
spec.Categories = Array.Empty<int>();
}
spec.SearchTerm = query.q;
spec.SearchTerm = query.q?.Trim();
spec.SearchType = query.t;
spec.Limit = query.limit;
spec.Offset = query.offset;
@@ -148,7 +153,7 @@ namespace NzbDrone.Core.IndexerSearch
return spec;
}
private async Task<List<ReleaseInfo>> Dispatch(Func<IIndexer, Task<IndexerPageableQueryResult>> searchAction, SearchCriteriaBase criteriaBase)
private async Task<IList<ReleaseInfo>> Dispatch(Func<IIndexer, Task<IndexerPageableQueryResult>> searchAction, SearchCriteriaBase criteriaBase)
{
var indexers = _indexerFactory.Enabled();
@@ -168,7 +173,7 @@ namespace NzbDrone.Core.IndexerSearch
if (indexers.Count == 0)
{
_logger.Debug("All provided categories are unsupported by selected indexers: {0}", string.Join(", ", criteriaBase.Categories));
return new List<ReleaseInfo>();
return Array.Empty<ReleaseInfo>();
}
}
@@ -189,7 +194,7 @@ namespace NzbDrone.Core.IndexerSearch
{
if (_indexerLimitService.AtQueryLimit((IndexerDefinition)indexer.Definition))
{
return new List<ReleaseInfo>();
return Array.Empty<ReleaseInfo>();
}
try
@@ -224,7 +229,7 @@ namespace NzbDrone.Core.IndexerSearch
_logger.Error(e, "Error while searching for {0}", criteriaBase);
}
return new List<ReleaseInfo>();
return Array.Empty<ReleaseInfo>();
}
}
}

View File

@@ -8,7 +8,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers.Cardigann;
using NzbDrone.Core.Indexers.Definitions.Cardigann;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -29,7 +29,7 @@ namespace NzbDrone.Core.IndexerVersions
/* Update Service will fall back if version # does not exist for an indexer per Ta */
private const string DEFINITION_BRANCH = "master";
private const int DEFINITION_VERSION = 8;
private const int DEFINITION_VERSION = 9;
// Used when moving yml to C#
private readonly List<string> _definitionBlocklist = new ()

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
public override string Name => "AudioBook Bay";
public override string[] IndexerUrls => new[]
{
"https://audiobookbay.li/",
"https://audiobookbay.is/",
"https://audiobookbay.se/"
};
public override string[] LegacyUrls => new[]
@@ -53,7 +53,8 @@ public class AudioBookBay : TorrentIndexerBase<NoAuthTorrentBaseSettings>
"https://audiobookbay.unblockit.page/",
"https://audiobookbay.unblockit.pet/",
"https://audiobookbay.unblockit.ink/",
"https://audiobookbay.unblockit.bio/" // error 502
"https://audiobookbay.unblockit.bio/", // error 502
"https://audiobookbay.li/"
};
public override string Description => "AudioBook Bay (ABB) is a public Torrent Tracker for AUDIOBOOKS";
public override string Language => "en-US";

View File

@@ -59,6 +59,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public class AvistaZParser : AvistazParserBase
{
protected override string TimezoneOffset => "+01:00";
protected override string TimezoneOffset => "+02:00";
}
}

View File

@@ -67,9 +67,9 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
_logger.Debug("Avistaz authentication succeeded.");
}
protected override bool CheckIfLoginNeeded(HttpResponse response)
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.PreconditionFailed;
return httpResponse.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.PreconditionFailed;
}
protected override void ModifyRequest(IndexerRequest request)

View File

@@ -13,18 +13,18 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
{
public class AvistazParserBase : IParseIndexerResponse
{
protected virtual string TimezoneOffset => "-05:00"; // Avistaz does not specify a timezone & returns server time
protected virtual string TimezoneOffset => "-04:00"; // Avistaz does not specify a timezone & returns server time
private readonly HashSet<string> _hdResolutions = new () { "1080p", "1080i", "720p" };
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<TorrentInfo>();
var releaseInfos = new List<ReleaseInfo>();
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.NotFound)
{
return torrentInfos.ToArray();
return releaseInfos.ToArray();
}
if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.TooManyRequests)
@@ -80,11 +80,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
release.TvdbId = row.MovieTvinfo.Tvdb.IsNullOrWhiteSpace() ? 0 : ParseUtil.TryCoerceInt(row.MovieTvinfo.Tvdb, out var tvdbResult) ? tvdbResult : 0;
}
torrentInfos.Add(release);
releaseInfos.Add(release);
}
// order by date
return torrentInfos.OrderByDescending(o => o.PublishDate).ToArray();
return releaseInfos
.OrderByDescending(o => o.PublishDate)
.ToArray();
}
// hook to adjust category parsing
@@ -115,7 +117,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
cats.Add(NewznabStandardCategory.Audio);
break;
default:
throw new Exception(string.Format("Error parsing Avistaz category type {0}", row.Type));
throw new Exception($"Error parsing Avistaz category type {row.Type}");
}
return cats;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
@@ -85,6 +86,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
request.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.Token}");
request.HttpRequest.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
yield return request;
}

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Configuration;
@@ -27,7 +28,6 @@ namespace NzbDrone.Core.Indexers.Definitions
public override string[] IndexerUrls => new[] { "https://bakabt.me/" };
public override string Description => "Anime Community";
private string LoginUrl => Settings.BaseUrl + "login.php";
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
public override IndexerCapabilities Capabilities => SetCapabilities();
@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var dom = parser.ParseDocument(response.Content);
var downloadLink = dom.QuerySelector(".download_link")?.GetAttribute("href");
if (string.IsNullOrWhiteSpace(downloadLink))
if (downloadLink.IsNullOrWhiteSpace())
{
throw new Exception("Unable to find download link.");
}
@@ -71,17 +71,19 @@ namespace NzbDrone.Core.Indexers.Definitions
{
UpdateCookies(null, null);
var requestBuilder = new HttpRequestBuilder(LoginUrl)
var loginUrl = Settings.BaseUrl + "login.php";
var requestBuilder = new HttpRequestBuilder(loginUrl)
{
LogResponseContent = true,
AllowAutoRedirect = true,
Method = HttpMethod.Post
};
var loginPage = await ExecuteAuth(new HttpRequest(LoginUrl));
var loginPage = await ExecuteAuth(new HttpRequest(loginUrl));
var parser = new HtmlParser();
var dom = parser.ParseDocument(loginPage.Content);
var dom = await parser.ParseDocumentAsync(loginPage.Content);
var loginKey = dom.QuerySelector("input[name=\"loginKey\"]");
if (loginKey != null)
{
@@ -98,21 +100,23 @@ namespace NzbDrone.Core.Indexers.Definitions
var response = await ExecuteAuth(authLoginRequest);
if (response.Content != null && response.Content.Contains("<a href=\"logout.php\">Logout</a>"))
if (CheckIfLoginNeeded(response))
{
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
var htmlParser = new HtmlParser();
var document = await htmlParser.ParseDocumentAsync(response.Content);
var errorMessage = document.QuerySelector("#loginError, .error")?.TextContent.Trim();
_logger.Debug("BakaBT authentication succeeded");
}
else
{
throw new IndexerAuthException("BakaBT authentication failed");
throw new IndexerAuthException(errorMessage ?? "Unknown error message, please report.");
}
UpdateCookies(response.GetCookies(), DateTime.Now.AddDays(30));
_logger.Debug("BakaBT authentication succeeded");
}
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
{
return !httpResponse.Content.Contains("<a href=\"logout.php\">Logout</a>");
return httpResponse.Content.Contains("loginForm");
}
private IndexerCapabilities SetCapabilities()
@@ -241,7 +245,7 @@ namespace NzbDrone.Core.Indexers.Definitions
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
{
var torrentInfos = new List<TorrentInfo>();
var releaseInfos = new List<ReleaseInfo>();
var parser = new HtmlParser();
var dom = parser.ParseDocument(indexerResponse.Content);
@@ -356,11 +360,11 @@ namespace NzbDrone.Core.Indexers.Definitions
release.DownloadVolumeFactor = row.QuerySelector("span.freeleech") != null ? 0 : 1;
release.UploadVolumeFactor = 1;
torrentInfos.Add(release);
releaseInfos.Add(release);
}
}
return torrentInfos.ToArray();
return releaseInfos.ToArray();
}
private ICollection<IndexerCategory> GetNextCategory(IElement row, ICollection<IndexerCategory> currentCategories)
@@ -388,12 +392,7 @@ namespace NzbDrone.Core.Indexers.Definitions
var categoryName = categoryElement.GetAttribute("title");
if (!string.IsNullOrWhiteSpace(categoryName))
{
return categoryName;
}
return null;
return categoryName.IsNotNullOrWhiteSpace() ? categoryName : null;
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Globalization;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -50,50 +50,63 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
var parameters = new BroadcastheNetTorrentQuery();
var searchString = searchCriteria.SearchTerm != null ? searchCriteria.SearchTerm : "";
var searchString = searchCriteria.SearchTerm ?? string.Empty;
var btnResults = searchCriteria.Limit.GetValueOrDefault();
if (btnResults == 0)
{
btnResults = (int)Capabilities.LimitsDefault;
btnResults = Capabilities.LimitsDefault.GetValueOrDefault(PageSize);
}
var btnOffset = searchCriteria.Offset.GetValueOrDefault();
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
if (searchCriteria.TvdbId > 0)
{
parameters.Tvdb = string.Format("{0}", searchCriteria.TvdbId);
parameters.Tvdb = $"{searchCriteria.TvdbId}";
}
if (searchCriteria.RId > 0)
else if (searchCriteria.RId > 0)
{
parameters.Tvrage = string.Format("{0}", searchCriteria.RId);
parameters.Tvrage = $"{searchCriteria.RId}";
}
else if (searchString.IsNotNullOrWhiteSpace())
{
parameters.Search = searchString.Replace(" ", "%");
}
// If only the season/episode is searched for then change format to match expected format
if (searchCriteria.Season > 0 && searchCriteria.Episode.IsNullOrWhiteSpace())
{
// Season Only
parameters.Name = string.Format("Season {0}%", searchCriteria.Season.Value);
// Search Season
parameters.Category = "Season";
parameters.Name = $"Season {searchCriteria.Season}%";
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
parameters = parameters.Clone();
// Search Episode
parameters.Category = "Episode";
parameters.Name = $"S{searchCriteria.Season:00}E%";
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
}
else if (searchCriteria.Season > 0 && Regex.IsMatch(searchCriteria.EpisodeSearchString, "(\\d{4}\\.\\d{2}\\.\\d{2})"))
else if (DateTime.TryParseExact($"{searchCriteria.Season} {searchCriteria.Episode}", "yyyy MM/dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var showDate))
{
// Daily Episode
parameters.Name = searchCriteria.EpisodeSearchString;
parameters.Name = showDate.ToString("yyyy.MM.dd");
parameters.Category = "Episode";
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
}
else if (searchCriteria.Season > 0 && int.Parse(searchCriteria.Episode) > 0)
else if (searchCriteria.Season > 0 && int.TryParse(searchCriteria.Episode, out var episode) && episode > 0)
{
// Standard (S/E) Episode
parameters.Name = string.Format("S{0:00}E{1:00}", searchCriteria.Season.Value, int.Parse(searchCriteria.Episode));
parameters.Name = $"S{searchCriteria.Season:00}E{episode:00}";
parameters.Category = "Episode";
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
}
else
{
// Neither a season only search nor daily nor standard, fall back to query
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
}
// Neither a season only search nor daily nor standard, fall back to query
parameters.Search = searchString.Replace(" ", "%");
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));
return pageableRequests;
}
@@ -109,17 +122,17 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
var parameters = new BroadcastheNetTorrentQuery();
var searchString = searchCriteria.SearchTerm != null ? searchCriteria.SearchTerm : "";
var searchString = searchCriteria.SearchTerm ?? "";
var btnResults = searchCriteria.Limit.GetValueOrDefault();
if (btnResults == 0)
{
btnResults = (int)Capabilities.LimitsDefault;
btnResults = Capabilities.LimitsDefault.GetValueOrDefault(PageSize);
}
parameters.Search = searchString.Replace(" ", "%");
var btnOffset = searchCriteria.Offset.GetValueOrDefault(0);
var btnOffset = searchCriteria.Offset.GetValueOrDefault();
parameters.Search = searchString.Replace(" ", "%");
pageableRequests.Add(GetPagedRequests(parameters, btnResults, btnOffset));

View File

@@ -6,11 +6,10 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Indexers.Definitions;
[Obsolete("Site unavailable")]
public class BrokenStones : GazelleBase<GazelleSettings>
{
public override string Name => "BrokenStones";
public override string[] IndexerUrls => new[] { "https://brokenstones.club/" };
public override string[] IndexerUrls => new[] { "https://brokenstones.is/" };
public override string Description => "Broken Stones is a Private site for MacOS and iOS APPS / GAMES";
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;

View File

@@ -14,7 +14,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Indexers.Cardigann
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
{
public class Cardigann : TorrentIndexerBase<CardigannSettings>
{

View File

@@ -15,7 +15,7 @@ using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Indexers.Cardigann
namespace NzbDrone.Core.Indexers.Definitions.Cardigann
{
public class CardigannBase
{
@@ -825,9 +825,19 @@ namespace NzbDrone.Core.Indexers.Cardigann
protected JArray JsonParseRowsSelector(JToken parsedJson, string rowSelector)
{
var selector = rowSelector.Split(':')[0];
var rowsObj = parsedJson.SelectToken(selector).Value<JArray>();
return new JArray(rowsObj.Where(t =>
JsonParseFieldSelector(t.Value<JObject>(), rowSelector.Remove(0, selector.Length)) != null));
try
{
var rowsObj = parsedJson.SelectToken(selector).Value<JArray>();
return new JArray(rowsObj.Where(t => JsonParseFieldSelector(t.Value<JObject>(), rowSelector.Remove(0, selector.Length)) != null));
}
catch (Exception ex)
{
_logger.Trace(ex, "Failed to parse JSON rows for selector \"{0}\"", rowSelector);
return null;
}
}
private string JsonParseFieldSelector(JToken parsedJson, string rowSelector)

Some files were not shown because too many files have changed in this diff Show More