1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

..

114 Commits

Author SHA1 Message Date
Bogdan
c331c8bd11 Ignore Grabbed from API docs
Run application in docs.sh specific to platform
2024-06-10 20:30:26 -07:00
Mark McDowall
52b72925f9 Fixed: Improve error messaging if config file isn't formatted correctly
Closes #6860
2024-06-10 20:30:13 -07:00
Mark McDowall
378fedcd9d Fixed: Skip invalid series paths during validation 2024-06-10 23:30:03 -04:00
Bogdan
a90ab1a8fd Fixed: Ignore case when resolving indexer by name in release push 2024-06-10 20:29:46 -07:00
Bogdan
0edc5ba99a Fixed: Ignore case for name validation in providers 2024-06-10 20:29:46 -07:00
Bogdan
ea54ade9bf New: Refresh cache for tracked queue on series add 2024-06-10 20:29:41 -07:00
Weblate
e07eb05e8b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlbertCoolGuy <Albert.rosenstand@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: xuzhihui <5894940@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-06-10 20:29:28 -07:00
Bogdan
d9b771ab0b Fixed: Error sending Manual Interaction Required when series is unknown 2024-05-31 20:11:31 -04:00
Mark McDowall
6b08e849b8 Search for raw and clean titles for Newznab/Torznab indexers that support raw title searching 2024-05-31 17:10:32 -07:00
Mark McDowall
9c1f48ebc9 Fixed: Include full series title in episode search 2024-05-31 17:10:32 -07:00
Mark McDowall
fd3dd1ab7d New: Genres and Images for Webhooks and Notifiarr
Closes #6822
2024-05-31 17:10:13 -07:00
yammes08
11e5c5a11b Fixed: SDR Files Being Parsed As HLG 2024-05-31 20:09:53 -04:00
Weblate
48f0291884 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: r0bertreh <Robert.reh@live.de>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translation: Servarr/Sonarr
2024-05-31 17:09:11 -07:00
Mark McDowall
af0e55aef4 Bump version to 4.0.5 2024-05-29 16:23:16 -07:00
Weblate
39a439eb4c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Bao Trinh <servarr@baodtrinh.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Co-authored-by: thegamingcat13 <sandervanbeek2004@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-29 16:23:04 -07:00
Weblate
66940b283b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-24 06:21:13 -07:00
Mark McDowall
2a662afaef Fixed: Time for episodes airing today being blank 2024-05-24 06:19:38 -07:00
Weblate
62a9c2519b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-22 20:09:18 -07:00
Mark McDowall
ca372bee25 Fixed: Queue and Calendar not loading 2024-05-22 20:07:31 -07:00
Sonarr
0904a0737e Automated API Docs update
ignore-downstream
2024-05-21 17:09:20 -07:00
Bogdan
70bc26dc19 Disable workflows on forks
ignore-downstream
2024-05-21 17:06:44 -07:00
Bogdan
a2e0002a08 Replace multiple occurrences in branch env variable
ignore-downstream
2024-05-21 17:06:44 -07:00
Bogdan
d7ceb11a64 Fixed: Trimming slashes from UrlBase when using environment variable 2024-05-21 17:06:44 -07:00
Bogdan
cc5b5463f2 Ignore Grabbed with STJson 2024-05-21 17:06:36 -07:00
Bogdan
9b4ff657af Update the wanted section for missing and cutoff unmet 2024-05-21 17:06:36 -07:00
Bogdan
aea50fa47e Bump Npgsql to 7.0.7
ignore-downstream
2024-05-21 17:06:28 -07:00
Mark McDowall
05edd44ed6 New: Include time for episode/season/series history 2024-05-21 17:06:18 -07:00
Bogdan
4440aa3cac New: Root folder exists validation for import lists 2024-05-21 17:06:09 -07:00
Bogdan
084fcc2295 Implement equality checks for providers 2024-05-21 17:05:48 -07:00
Weblate
536ff142c3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Ransack6086 <servarr.jubilant150@slmail.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yi Cao <caoyi06@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: topnew <sznetim@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-21 17:05:34 -07:00
Mark McDowall
627b2a4289 New: Parse 480i Bluray/Remux as Bluray 480p
Closes #6801
2024-05-09 22:04:18 -07:00
Bogdan
9734c2d144 Fixed: Notifications with only On Rename enabled
ignore-downstream
2024-05-09 22:04:12 -07:00
Bogdan
c7c1e3ac9e Refactor PasswordInput to use type password 2024-05-09 22:04:04 -07:00
Bogdan
429444d085 Fixed: Text color for inputs on login page 2024-05-09 22:03:56 -07:00
Mark McDowall
5cb649e9d8 Fixed: Attempt to parse and reject ambiguous dates
Closes #6799
2024-05-09 22:03:44 -07:00
Mark McDowall
cac7d239ea Fixed: Parsing of partial season pack 2024-05-09 22:03:44 -07:00
Sonarr
3940059ea3 Automated API Docs update
ignore-downstream
2024-05-09 22:03:31 -07:00
Weblate
20d00fe88c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-05-09 22:03:24 -07:00
Mark McDowall
b4d05214ae Fixed: Ignore invalid movie tags when writing XBMC metadata
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-05-08 18:45:19 -07:00
Mark McDowall
cc0a284660 New: Add series tags to Webhook and Notifiarr events 2024-05-08 18:45:19 -07:00
Mark McDowall
f50a263f4f New: Add Custom Format Score to file in Episode Details 2024-05-08 18:45:03 -07:00
Mark McDowall
29176c8367 New: Has Unmonitored Season filter for Series 2024-05-08 18:44:52 -07:00
Bogdan
1eddf3a152 Use number input for seed ratio 2024-05-08 18:44:36 -07:00
Bogdan
8360dd7a7b Fixed: Parsing long downloading/seeding values from Transmission 2024-05-08 18:44:27 -07:00
Bogdan
7e8d8500f2 Fixed: Next/previous/last air dates with Postgres DB
Closes #6790
2024-05-08 18:43:51 -07:00
Mark McDowall
cae134ec7b New: Dark theme for login screen
Closes #6751
2024-05-08 18:42:54 -07:00
Stevie Robinson
f81bb3ec19 New: Blocklist Custom Filters
Closes #6763
2024-05-08 18:42:41 -07:00
Bogdan
128309068d Fixed: Initialize databases after app folder migrations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-05-08 18:42:15 -07:00
Mickaël Thomas
73a4bdea52 New: Support stoppedUP and stoppedDL states from qBittorrent 2024-05-08 18:41:59 -07:00
Bogdan
47ba002806 Fixed: Indexer flags for torrent release pushes 2024-05-04 18:56:52 -07:00
Mark McDowall
ba88185dea New: Treat batch releases with total episode count as full season release
Closes #6757
2024-05-04 18:56:15 -07:00
Mark McDowall
e24ce40eb8 Fixed: History with unknown episode
Closes #6782
2024-05-04 18:55:56 -07:00
Stevie Robinson
8be8c7f89c Add missing translation key 2024-05-04 18:55:44 -07:00
Bogdan
7166a6c019 Parameter binding for API requests 2024-05-04 18:55:12 -07:00
Mark McDowall
3fbe436138 Forward X-Forwarded-Host header
Closes #6764
2024-05-04 18:54:55 -07:00
Jared
92eab4b2e2 New: Config file setting to disable log database
Closes #6743
2024-05-04 18:54:42 -07:00
Mika
23c741fd00 Add file-count for Transmission RPC 2024-05-04 18:53:47 -07:00
Weblate
8ddf46113b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-05-04 18:53:15 -07:00
Bogdan
c81ae65461 Fixed: Limit titles in task name to 10 series 2024-04-27 18:09:08 -07:00
Bogdan
efb3fa93e4 Fixed: Retrying download for pushed releases
ignore-downstream
#6752
2024-04-27 21:08:40 -04:00
Stevie Robinson
04bd535cfc New: Don't initially select 0 byte files in Interactive Import
Closes #6686
2024-04-27 21:07:41 -04:00
Bogdan
9738101042 Treat CorruptDatabaseException as a startup failure
ignore-downstream
2024-04-27 18:06:49 -07:00
Bogdan
1df7cdc65e New: Add KRaLiMaRKo and BluDragon to release group parsing exceptions
ignore-downstream
2024-04-27 18:06:38 -07:00
Jared
d051dac12c New: Optionally use Environment Variables for settings in config.xml
Closes #6744
2024-04-27 21:06:26 -04:00
Bogdan
5d01ecd30e Bump frontend dependencies
ignore-downstream
2024-04-27 18:05:16 -07:00
Mark McDowall
316b5cbf75 New: Validate that folders in paths don't start or end with a space
Closes #6709
2024-04-27 21:04:50 -04:00
Bogdan
2440672179 Bump SixLabors.ImageSharp to 3.1.4
ignore-downstream
2024-04-27 18:04:35 -07:00
Mark McDowall
a97fbcc40a Fixed: Improve paths longer than 256 on Windows failing to hardlink 2024-04-27 18:04:26 -07:00
Christopher
d738035fed New: Remove qBitorrent torrents that reach inactive seeding time 2024-04-27 21:04:16 -04:00
Mark McDowall
dc3e932102 macOS tests now run on arm64 2024-04-27 18:03:12 -07:00
Weblate
aded9d95f7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: maodun96 <435795439@qq.com>
Co-authored-by: toeiazarothis <patrickdealmeida89000@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-04-27 18:03:03 -07:00
Mark McDowall
b81c3ee4a8 Fix labeling config 2024-04-15 21:14:49 -07:00
Mark McDowall
cf6748a80c Fix merge conflict labeling 2024-04-15 21:14:49 -07:00
Sonarr
ef6cc7fa3a Automated API Docs update
ignore-downstream
2024-04-15 20:35:29 -07:00
Mark McDowall
f9b013a8bf New: Parse releases with multiple Ukranian audio tracks
Closes #6714
2024-04-15 20:25:41 -07:00
Bogdan
e966254462 Fixed: Re-testing edited providers will forcibly test them 2024-04-15 20:25:31 -07:00
Gauvino
016c4b353b Add merge conflict labeler 2024-04-15 23:25:13 -04:00
Uruk
d71c619f1a Update CI dependencies 2024-04-15 20:24:20 -07:00
Gauthier
6c232b062c New: Multi Language selection per indexer
Closes #2854
2024-04-15 23:24:05 -04:00
Josh McKinney
d6278fced4 Add dev container workspace
Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace
2024-04-15 20:23:13 -07:00
Weblate
317ce39aa2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Altair <villagermd@outlook.com>
Co-authored-by: Fonkio <maxime.fabre10@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Jacopo Luca Maria Latrofa <jacopo.latrofa@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-04-15 20:23:04 -07:00
Mark McDowall
941985f65b Bump version to 4.0.4 2024-04-13 10:05:01 -07:00
Mark McDowall
10daf97d81 Improve build step dependencies 2024-04-12 16:00:46 -07:00
Mark McDowall
6b08117d7d Improve release notes for main releases 2024-04-12 16:00:46 -07:00
Weblate
9afe1c4b3f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: YSLG <1451164040@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-04-12 16:00:37 -07:00
Mark McDowall
0fdbbd018c New: Parse absolute episode numbers within square brackets
Closes #6694
2024-04-12 16:00:16 -07:00
Sonarr
8a7b67c593 Automated API Docs update
ignore-downstream
2024-04-09 16:17:48 -07:00
Weblate
4b8afe3d33 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: myrad2267 <myrad2267@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-04-09 16:16:11 -07:00
Bogdan
476e7a7b94 Fixed: Changing Release Type in Manage Episodes
Closes #6706
2024-04-09 19:14:56 -04:00
Bogdan
1fcd2b492c Prevent multiple enumerations in Custom Formats token 2024-04-09 16:14:33 -07:00
Bogdan
1aef91041e New: Detect shfs mounts in disk space 2024-04-09 16:14:16 -07:00
Bogdan
fc06e51352 Fixed: Renaming episodes for a series
Closes #6640
2024-04-09 19:13:59 -04:00
Mark McDowall
f4c19a384b New: Auto tag series based on tags present/absent on series
Closes #6236
2024-04-09 16:13:30 -07:00
Josh McKinney
5061dc4b5e Add DevContainer, VSCode config and extensions.json 2024-04-09 19:12:58 -04:00
Mark McDowall
37863a8deb New: Option to prefix app name on Telegram notification titles 2024-04-09 19:12:20 -04:00
Mark McDowall
5c42935eb3 Fixed: Improve AniList testing with Media filters 2024-04-09 16:12:05 -07:00
Weblate
dac69445e4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-04-05 23:13:02 -07:00
Qstick
aca10f6f4f Fixed: Skip move when source and destination are the same
ignore-downstream

Co-Authored-By: Colin Hebert <makkhdyn@gmail.com>
(cherry picked from commit 7a5ae56a96700f401726ac80b3031a25207d8f75)
2024-04-05 23:11:37 -07:00
Mark McDowall
74cdf01e49 New: Set 'Release Type' during Manual Import
Closes #6681
2024-04-06 02:11:17 -04:00
Mark McDowall
a169ebff2a Fixed: Sending ntfy.sh notifications with unicode characters
Closes #6679
2024-04-06 02:11:03 -04:00
fireph
7fc3bebc91 New: Footnote to indicate some renaming tokens support truncation 2024-04-06 02:10:42 -04:00
Till Krüss
e672996dbb Improve text for file deleted through UI/API 2024-04-06 02:09:55 -04:00
Stevie Robinson
238ba85f0a New: Informational text on Custom Formats modal 2024-04-06 02:08:57 -04:00
Cuki
1562d3bae3 Fixed: Use widely supported display mode for PWA 2024-04-06 02:08:08 -04:00
Jendrik Weise
7776ec9955 Reimport files imported prematurely during script import 2024-04-05 23:07:38 -07:00
Jendrik Weise
af5a681ab7 Fix ignoring title based on pre-rename episodefile 2024-04-05 23:07:38 -07:00
Jendrik Weise
0a7f3a12c2 Do not remove all extras when script importing 2024-04-05 23:07:38 -07:00
Jendrik Weise
2ef46e5b90 Fix incorrect subtitle copy regex 2024-04-05 23:07:38 -07:00
Mark McDowall
6003ca1696 Fixed: Deleted episodes not being unmonitored when series folder has been deleted
Closes #6678
2024-04-05 23:07:07 -07:00
Mark McDowall
0937ee6fef Fixed: Path parsing incorrectly treating series title as episode number 2024-04-05 23:06:56 -07:00
Mark McDowall
60ee7cc716 Fixed: Cleanse BHD RSS key in log files
Closes #6666
2024-04-06 02:06:35 -04:00
Mark McDowall
4e83820511 Bump version to 4.0.3 2024-03-31 21:52:19 -07:00
Sonarr
5a66b949cf Automated API Docs update
ignore-downstream
2024-03-31 21:43:33 -07:00
Weblate
f010f56290 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: 王锋 <17611382361@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-03-31 21:43:09 -07:00
300 changed files with 6732 additions and 3206 deletions

View File

@@ -0,0 +1,13 @@
// This file is used to open the backend and frontend in the same workspace, which is necessary as
// the frontend has vscode settings that are distinct from the backend
{
"folders": [
{
"path": ".."
},
{
"path": "../frontend"
}
],
"settings": {}
}

View File

@@ -0,0 +1,19 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "Sonarr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,
"version": "16",
"nvmVersion": "latest"
}
},
"forwardPorts": [8989],
"customizations": {
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
}
}

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

18
.github/labeler.yml vendored
View File

@@ -1,17 +1,23 @@
'connection':
- src/NzbDrone.Core/Notifications/**/*
- changed-files:
- any-glob-to-any-file: src/NzbDrone.Core/Notifications/**/*
'db-migration':
- src/NzbDrone.Core/Datastore/Migration/*
- changed-files:
- any-glob-to-any-file: src/NzbDrone.Core/Datastore/Migration/*
'download-client':
- src/NzbDrone.Core/Download/Clients/**/*
- changed-files:
- any-glob-to-any-file: src/NzbDrone.Core/Download/Clients/**/*
'indexer':
- src/NzbDrone.Core/Indexers/**/*
- changed-files:
- any-glob-to-any-file: src/NzbDrone.Core/Indexers/**/*
'parsing':
- src/NzbDrone.Core/Parser/**/*
- changed-files:
- any-glob-to-any-file: src/NzbDrone.Core/Parser/**/*
'ui-only':
- all: ['frontend/**/*']
- changed-files:
- any-glob-to-all-files: frontend/**/*

View File

@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4
VERSION: 4.0.2
VERSION: 4.0.5
jobs:
backend:
@@ -48,7 +48,7 @@ jobs:
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
echo "BRANCH=${RAW_BRANCH_NAME/\//-}" >> "$GITHUB_ENV"
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
@@ -76,11 +76,11 @@ jobs:
framework: ${{ env.FRAMEWORK }}
runtime: linux-x64
- name: Publish osx-x64 Test Artifact
- name: Publish osx-arm64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: osx-x64
runtime: osx-arm64
# Build Artifacts (grouped by OS)
@@ -143,7 +143,7 @@ jobs:
artifact: tests-linux-x64
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
- os: macos-latest
artifact: tests-osx-x64
artifact: tests-osx-arm64
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
- os: windows-latest
artifact: tests-win-x64
@@ -190,10 +190,10 @@ jobs:
binary_artifact: build_linux
binary_path: linux-x64/${{ needs.backend.outputs.framework }}/Sonarr
- os: macos-latest
artifact: tests-osx-x64
artifact: tests-osx-arm64
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
binary_artifact: build_macos
binary_path: osx-x64/${{ needs.backend.outputs.framework }}/Sonarr
binary_path: osx-arm64/${{ needs.backend.outputs.framework }}/Sonarr
- os: windows-latest
artifact: tests-win-x64
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest
@@ -217,7 +217,7 @@ jobs:
deploy:
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
needs: [backend, unit_test, unit_test_postgres, integration_test]
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test]
secrets: inherit
uses: ./.github/workflows/deploy.yml
with:
@@ -228,7 +228,7 @@ jobs:
notify:
name: Discord Notification
needs: [backend, unit_test, unit_test_postgres, integration_test]
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy]
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
env:
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
@@ -236,7 +236,7 @@ jobs:
steps:
- name: Notify
uses: tsickert/discord-webhook@v5.3.0
uses: tsickert/discord-webhook@v6.0.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
username: 'GitHub Actions'

26
.github/workflows/conflict_labeler.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Merge Conflict Labeler
on:
push:
branches:
- develop
pull_request_target:
branches:
- develop
types: [synchronize]
jobs:
label:
name: Labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'Sonarr/Sonarr' }}
permissions:
contents: read
pull-requests: write
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@v3
with:
dirtyLabel: 'merge-conflict'
repoToken: '${{ secrets.GITHUB_TOKEN }}'

View File

@@ -69,12 +69,38 @@ jobs:
pattern: release_*
merge-multiple: true
- name: Get Previous Release
id: previous-release
uses: cardinalby/git-get-release-action@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
latest: true
prerelease: ${{ inputs.branch != 'main' }}
- name: Generate Release Notes
id: generate-release-notes
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
result-encoding: string
script: |
const { data } = await github.rest.repos.generateReleaseNotes({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: 'v${{ inputs.version }}',
target_commitish: '${{ github.sha }}',
previous_tag_name: '${{ steps.previous-release.outputs.tag_name }}',
})
return data.body
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: _artifacts/Sonarr.*
commit: ${{ github.sha }}
generateReleaseNotes: true
generateReleaseNotes: false
body: ${{ steps.generate-release-notes.outputs.result }}
name: ${{ inputs.version }}
prerelease: ${{ inputs.branch != 'main' }}
skipIfReleaseExists: true

View File

@@ -8,5 +8,6 @@ jobs:
contents: read
pull-requests: write
runs-on: ubuntu-latest
if: github.repository == 'Sonarr/Sonarr'
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5

View File

@@ -8,14 +8,15 @@ on:
jobs:
lock:
runs-on: ubuntu-latest
if: github.repository == 'Sonarr/Sonarr'
steps:
- uses: dessant/lock-threads@v2
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: 'one-day-maybe'
issue-lock-labels: ''
issue-lock-comment: ''
issue-inactive-days: '90'
exclude-issue-created-before: ''
exclude-any-issue-labels: 'one-day-maybe'
add-issue-labels: ''
issue-comment: ''
issue-lock-reason: 'resolved'
process-only: ''

1
.gitignore vendored
View File

@@ -127,6 +127,7 @@ coverage*.xml
coverage*.json
setup/Output/
*.~is
.mono
#VS outout folders
bin

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ms-dotnettools.csdevkit",
"ms-vscode-remote.remote-containers"
]
}

26
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": "Run Sonarr",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build dotnet",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/_output/net6.0/Sonarr",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

44
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,44 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build dotnet",
"command": "dotnet",
"type": "process",
"args": [
"msbuild",
"-restore",
"${workspaceFolder}/src/Sonarr.sln",
"-p:GenerateFullPaths=true",
"-p:Configuration=Debug",
"-p:Platform=Posix",
"-consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/Sonarr.sln",
"-property:GenerateFullPaths=true",
"-consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Sonarr.sln"
],
"problemMatcher": "$msCompile"
}
]
}

12
docs.sh
View File

@@ -25,17 +25,23 @@ slnFile=src/Sonarr.sln
platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Sonarr.Console.dll
else
application=Sonarr.dll
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 &
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
sleep 30
sleep 45
kill %1

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -20,6 +21,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlocklistFilterModal from './BlocklistFilterModal';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
@@ -114,9 +116,13 @@ class Blocklist extends Component {
error,
items,
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onFilterSelect,
...otherProps
} = this.props;
@@ -161,6 +167,15 @@ class Blocklist extends Component {
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={BlocklistFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
@@ -180,7 +195,11 @@ class Blocklist extends Component {
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoHistoryBlocklist')}
{
selectedFilterKey === 'all' ?
translate('NoHistoryBlocklist') :
translate('BlocklistFilterHasNoItems')
}
</Alert>
}
@@ -251,11 +270,15 @@ Blocklist.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
onClearBlocklistPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
@@ -13,10 +14,12 @@ import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCustomFiltersSelector('blocklist'),
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
(blocklist, customFilters, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
customFilters,
...blocklist
};
}
@@ -97,6 +100,10 @@ class BlocklistConnector extends Component {
this.props.setBlocklistSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setBlocklistFilter({ selectedFilterKey });
};
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
};
@@ -122,6 +129,7 @@ class BlocklistConnector extends Component {
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
@@ -142,6 +150,7 @@ BlocklistConnector.propTypes = {
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistFilter: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
function createBlocklistSelector() {
return createSelector(
(state: AppState) => state.blocklist.items,
(blocklistItems) => {
return blocklistItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.blocklist.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface BlocklistFilterModalProps {
isOpen: boolean;
}
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
const sectionItems = useSelector(createBlocklistSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'blocklist';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setBlocklistFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -24,7 +24,11 @@ function TimeleftCell(props) {
} = props;
if (status === 'delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
@@ -40,7 +44,11 @@ function TimeleftCell(props) {
}
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (

View File

@@ -12,11 +12,10 @@ function App({ store, history }) {
<DocumentTitle title={window.Sonarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
<ApplyTheme />
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View File

@@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Sonarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View File

@@ -0,0 +1,37 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
Object.entries(themes[theme]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
export default ApplyTheme;

View File

@@ -1,4 +1,5 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import EpisodeFilesAppState from './EpisodeFilesAppState';
@@ -54,6 +55,7 @@ export interface AppSectionState {
interface AppState {
app: AppSectionState;
blocklist: BlocklistAppState;
calendar: CalendarAppState;
commands: CommandAppState;
episodeFiles: EpisodeFilesAppState;

View File

@@ -0,0 +1,8 @@
import Blocklist from 'typings/Blocklist';
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
interface BlocklistAppState
extends AppSectionState<Blocklist>,
AppSectionFilterState<Blocklist> {}
export default BlocklistAppState;

View File

@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
if (view === calendarViews.WEEK) {
formatedDate = momentDate.format(calendarWeekColumnHeader);
} else if (view === calendarViews.FORECAST) {
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
}
return (

View File

@@ -22,6 +22,7 @@ import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import SeriesTagInput from './SeriesTagInput';
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import TagInputConnector from './TagInputConnector';
import TagSelectInputConnector from './TagSelectInputConnector';
@@ -87,6 +88,9 @@ function getComponent(type) {
case inputTypes.DYNAMIC_SELECT:
return EnhancedSelectInputConnector;
case inputTypes.SERIES_TAG:
return SeriesTagInput;
case inputTypes.SERIES_TYPE_SELECT:
return SeriesTypeSelectInput;

View File

@@ -1,5 +0,0 @@
.input {
composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'input': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,7 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
@@ -13,17 +11,14 @@ function PasswordInput(props) {
return (
<TextInput
{...props}
type="password"
onCopy={onCopy}
/>
);
}
PasswordInput.propTypes = {
className: PropTypes.string.isRequired
};
PasswordInput.defaultProps = {
className: styles.input
...TextInput.props
};
export default PasswordInput;

View File

@@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.DYNAMIC_SELECT;
}
return inputTypes.SELECT;
case 'seriesTag':
return inputTypes.SERIES_TAG;
case 'tag':
return inputTypes.TEXT_TAG;
case 'tagSelect':

View File

@@ -0,0 +1,53 @@
import React, { useCallback } from 'react';
import TagInputConnector from './TagInputConnector';
interface SeriesTageInputProps {
name: string;
value: number | number[];
onChange: ({
name,
value,
}: {
name: string;
value: number | number[];
}) => void;
}
export default function SeriesTagInput(props: SeriesTageInputProps) {
const { value, onChange, ...otherProps } = props;
const isArray = Array.isArray(value);
const handleChange = useCallback(
({ name, value: newValue }: { name: string; value: number[] }) => {
if (isArray) {
onChange({ name, value: newValue });
} else {
onChange({
name,
value: newValue.length ? newValue[newValue.length - 1] : 0,
});
}
},
[isArray, onChange]
);
let finalValue: number[] = [];
if (isArray) {
finalValue = value;
} else if (value === 0) {
finalValue = [];
} else {
finalValue = [value];
}
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
<TagInputConnector
{...otherProps}
value={finalValue}
onChange={handleChange}
/>
);
}

View File

@@ -244,7 +244,7 @@ class SignalRConnector extends Component {
handleWantedCutoff = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'cutoffUnmet',
section: 'wanted.cutoffUnmet',
updateOnly: true,
...body.resource
});
@@ -254,7 +254,7 @@ class SignalRConnector extends Component {
handleWantedMissing = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'missing',
section: 'wanted.missing',
updateOnly: true,
...body.resource
});

View File

@@ -15,6 +15,7 @@ class RelativeDateCell extends PureComponent {
className,
date,
includeSeconds,
includeTime,
showRelativeDates,
shortDateFormat,
longDateFormat,
@@ -39,7 +40,7 @@ class RelativeDateCell extends PureComponent {
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
{getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })}
</Component>
);
}
@@ -49,6 +50,7 @@ RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired,
date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired,
includeTime: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
@@ -60,6 +62,7 @@ RelativeDateCell.propTypes = {
RelativeDateCell.defaultProps = {
className: styles.cell,
includeSeconds: false,
includeTime: false,
component: TableRowCell
};

View File

@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
}
/*
* text-security-disc
*/
@font-face {
font-weight: normal;
font-style: normal;
font-family: 'text-security-disc';
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
}

View File

@@ -15,5 +15,5 @@
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "minimal-ui"
"display": "standalone"
}

View File

@@ -111,6 +111,8 @@ class EpisodeHistoryRow extends Component {
<RelativeDateCellConnector
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>

View File

@@ -17,6 +17,12 @@
width: 175px;
}
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 65px;
}
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'customFormatScore': string;
'customFormats': string;
'languages': string;
'quality': string;

View File

@@ -11,6 +11,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import MediaInfo from './MediaInfo';
import styles from './EpisodeFileRow.css';
@@ -55,6 +56,7 @@ class EpisodeFileRow extends Component {
languages,
quality,
customFormats,
customFormatScore,
qualityCutoffNotMet,
mediaInfo,
columns
@@ -127,6 +129,17 @@ class EpisodeFileRow extends Component {
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
@@ -183,6 +196,7 @@ EpisodeFileRow.propTypes = {
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeleteEpisodeFile: PropTypes.func.isRequired

View File

@@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds, sizes } from 'Helpers/Props';
import { icons, kinds, sizes } from 'Helpers/Props';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import translate from 'Utilities/String/translate';
import EpisodeAiringConnector from './EpisodeAiringConnector';
@@ -42,6 +43,15 @@ const columns = [
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'actions',
label: '',
@@ -94,6 +104,7 @@ class EpisodeSummary extends Component {
languages,
quality,
customFormats,
customFormatScore,
qualityCutoffNotMet,
onDeleteEpisodeFile
} = this.props;
@@ -143,6 +154,7 @@ class EpisodeSummary extends Component {
quality={quality}
qualityCutoffNotMet={qualityCutoffNotMet}
customFormats={customFormats}
customFormatScore={customFormatScore}
mediaInfo={mediaInfo}
columns={columns}
onDeleteEpisodeFile={onDeleteEpisodeFile}
@@ -179,6 +191,7 @@ EpisodeSummary.propTypes = {
quality: PropTypes.object,
qualityCutoffNotMet: PropTypes.bool,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
onDeleteEpisodeFile: PropTypes.func.isRequired
};

View File

@@ -31,7 +31,8 @@ function createMapStateToProps() {
languages,
quality,
qualityCutoffNotMet,
customFormats
customFormats,
customFormatScore
} = episodeFile;
return {
@@ -45,7 +46,8 @@ function createMapStateToProps() {
languages,
quality,
qualityCutoffNotMet,
customFormats
customFormats,
customFormatScore
};
}
);

View File

@@ -0,0 +1,17 @@
import ReleaseType from 'InteractiveImport/ReleaseType';
import translate from 'Utilities/String/translate';
export default function getReleaseTypeName(
releaseType?: ReleaseType
): string | null {
switch (releaseType) {
case 'singleEpisode':
return translate('SingleEpisode');
case 'multiEpisode':
return translate('MultiEpisode');
case 'seasonPack':
return translate('SeasonPack');
default:
return translate('Unknown');
}
}

View File

@@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import ReleaseType from 'InteractiveImport/ReleaseType';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
@@ -17,6 +18,7 @@ export interface EpisodeFile extends ModelBase {
quality: QualityModel;
customFormats: CustomFormat[];
indexerFlags: number;
releaseType: ReleaseType;
mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean;
}

View File

@@ -17,6 +17,7 @@ export const LANGUAGE_SELECT = 'languageSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
export const SERIES_TAG = 'seriesTag';
export const DYNAMIC_SELECT = 'dynamicSelect';
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
export const TAG = 'tag';
@@ -45,6 +46,7 @@ export const all = [
ROOT_FOLDER_SELECT,
LANGUAGE_SELECT,
SELECT,
SERIES_TAG,
DYNAMIC_SELECT,
SERIES_TYPE_SELECT,
TAG,

View File

@@ -36,6 +36,7 @@ import InteractiveImport, {
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
@@ -73,7 +74,8 @@ type SelectType =
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
| 'indexerFlags'
| 'releaseType';
type FilterExistingFiles = 'all' | 'new';
@@ -128,6 +130,12 @@ const COLUMNS = [
isSortable: true,
isVisible: true,
},
{
name: 'releaseType',
label: () => translate('ReleaseType'),
isSortable: true,
isVisible: true,
},
{
name: 'customFormats',
label: React.createElement(Icon, {
@@ -369,6 +377,10 @@ function InteractiveImportModalContent(
key: 'indexerFlags',
value: translate('SelectIndexerFlags'),
},
{
key: 'releaseType',
value: translate('SelectReleaseType'),
},
];
if (allowSeriesChange) {
@@ -511,6 +523,7 @@ function InteractiveImportModalContent(
languages,
indexerFlags,
episodeFileId,
releaseType,
} = item;
if (!series) {
@@ -560,6 +573,7 @@ function InteractiveImportModalContent(
quality,
languages,
indexerFlags,
releaseType,
});
return;
@@ -575,6 +589,7 @@ function InteractiveImportModalContent(
quality,
languages,
indexerFlags,
releaseType,
downloadId,
episodeFileId,
});
@@ -787,6 +802,22 @@ function InteractiveImportModalContent(
[selectedIds, dispatch]
);
const onReleaseTypeSelect = useCallback(
(releaseType: string) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const orderedSelectedIds = items.reduce((acc: number[], file) => {
if (selectedIds.includes(file.id)) {
acc.push(file.id);
@@ -1000,6 +1031,14 @@ function InteractiveImportModalContent(
onModalClose={onSelectModalClose}
/>
<SelectReleaseTypeModal
isOpen={selectModalOpen === 'releaseType'}
releaseType="unknown"
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}

View File

@@ -12,6 +12,7 @@ import Episode from 'Episode/Episode';
import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import getReleaseTypeName from 'Episode/getReleaseTypeName';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
@@ -20,6 +21,8 @@ import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexe
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
@@ -44,7 +47,8 @@ type SelectType =
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags';
| 'indexerFlags'
| 'releaseType';
type SelectedChangeProps = SelectStateInputProps & {
hasEpisodeFileId: boolean;
@@ -61,6 +65,7 @@ interface InteractiveImportRowProps {
quality?: QualityModel;
languages?: Language[];
size: number;
releaseType: ReleaseType;
customFormats?: object[];
customFormatScore?: number;
indexerFlags: number;
@@ -86,6 +91,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
languages,
releaseGroup,
size,
releaseType,
customFormats,
customFormatScore,
indexerFlags,
@@ -122,7 +128,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
seasonNumber != null &&
episodes.length &&
quality &&
languages
languages &&
size > 0
) {
onSelectedChange({
id,
@@ -315,6 +322,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectReleaseTypePress = useCallback(() => {
setSelectModalOpen('releaseType');
}, [setSelectModalOpen]);
const onReleaseTypeSelect = useCallback(
(releaseType: ReleaseType) => {
dispatch(
updateInteractiveImportItem({
id,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectIndexerFlagsPress = useCallback(() => {
setSelectModalOpen('indexerFlags');
}, [setSelectModalOpen]);
@@ -461,6 +489,13 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
<TableRowCell>{formatBytes(size)}</TableRowCell>
<TableRowCellButton
title={translate('ClickToChangeReleaseType')}
onPress={onSelectReleaseTypePress}
>
{getReleaseTypeName(releaseType)}
</TableRowCellButton>
<TableRowCell>
{customFormats?.length ? (
<Popover
@@ -572,6 +607,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
onModalClose={onSelectModalClose}
/>
<SelectReleaseTypeModal
isOpen={selectModalOpen === 'releaseType'}
releaseType={releaseType ?? 'unknown'}
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}

View File

@@ -15,6 +15,7 @@ export interface InteractiveImportCommandOptions {
quality: QualityModel;
languages: Language[];
indexerFlags: number;
releaseType: ReleaseType;
downloadId?: string;
episodeFileId?: number;
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModalContent from './SelectReleaseTypeModalContent';
interface SelectQualityModalProps {
isOpen: boolean;
releaseType: ReleaseType;
modalTitle: string;
onReleaseTypeSelect(releaseType: ReleaseType): void;
onModalClose(): void;
}
function SelectReleaseTypeModal(props: SelectQualityModalProps) {
const { isOpen, releaseType, modalTitle, onReleaseTypeSelect, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectReleaseTypeModalContent
releaseType={releaseType}
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectReleaseTypeModal;

View File

@@ -0,0 +1,99 @@
import React, { useCallback, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import ReleaseType from 'InteractiveImport/ReleaseType';
import translate from 'Utilities/String/translate';
const options = [
{
key: 'unknown',
get value() {
return translate('Unknown');
},
},
{
key: 'singleEpisode',
get value() {
return translate('SingleEpisode');
},
},
{
key: 'multiEpisode',
get value() {
return translate('MultiEpisode');
},
},
{
key: 'seasonPack',
get value() {
return translate('SeasonPack');
},
},
];
interface SelectReleaseTypeModalContentProps {
releaseType: ReleaseType;
modalTitle: string;
onReleaseTypeSelect(releaseType: ReleaseType): void;
onModalClose(): void;
}
function SelectReleaseTypeModalContent(
props: SelectReleaseTypeModalContentProps
) {
const { modalTitle, onReleaseTypeSelect, onModalClose } = props;
const [releaseType, setReleaseType] = useState(props.releaseType);
const handleReleaseTypeChange = useCallback(
({ value }: { value: string }) => {
setReleaseType(value as ReleaseType);
},
[setReleaseType]
);
const handleReleaseTypeSelect = useCallback(() => {
onReleaseTypeSelect(releaseType);
}, [releaseType, onReleaseTypeSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectReleaseType')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('ReleaseType')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="releaseType"
value={releaseType}
values={options}
onChange={handleReleaseTypeChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={handleReleaseTypeSelect}>
{translate('SelectReleaseType')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectReleaseTypeModalContent;

View File

@@ -14,7 +14,7 @@ function SeriesHistoryModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose}
>
<SeriesHistoryModalContentConnector

View File

@@ -86,6 +86,10 @@ class SeriesHistoryRow extends Component {
const EpisodeComponent = fullSeries ? SeasonEpisodeNumber : EpisodeNumber;
if (!series || !episode) {
return null;
}
return (
<TableRow>
<HistoryEventTypeCell
@@ -131,6 +135,8 @@ class SeriesHistoryRow extends Component {
<RelativeDateCellConnector
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>

View File

@@ -138,7 +138,10 @@ function getInfoRowProps(
}),
iconName: icons.CALENDAR,
label:
getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
}) ?? '',
@@ -156,7 +159,10 @@ function getInfoRowProps(
}),
iconName: icons.ADD,
label:
getRelativeDate(added, shortDateFormat, showRelativeDates, {
getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
}) ?? '',
@@ -232,15 +238,13 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
<SeriesIndexOverviewInfoRow
title={formatDateTime(nextAiring, longDateFormat, timeFormat)}
iconName={icons.SCHEDULED}
label={getRelativeDate(
nextAiring,
label={getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true,
}
)}
timeFormat,
timeForToday: true,
})}
/>
)}

View File

@@ -217,7 +217,10 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
timeFormat
)}`}
>
{getRelativeDate(nextAiring, shortDateFormat, showRelativeDates, {
{getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
})}

View File

@@ -80,7 +80,10 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
timeFormat
)}`}
>
{getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
{getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
})}
@@ -89,15 +92,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
}
if (sortKey === 'added' && added) {
const addedDate = getRelativeDate(
added,
const addedDate = getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false,
}
);
timeFormat,
timeForToday: false,
});
return (
<div

View File

@@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
</Form>
<FieldSet legend={translate('Conditions')}>
<Alert kind={kinds.INFO}>
<div>
{translate('CustomFormatsSettingsTriggerInfo')}
</div>
</Alert>
<div className={styles.customFormats}>
{
specifications.map((tag) => {

View File

@@ -80,19 +80,19 @@ const fileNameTokens = [
];
const seriesTokens = [
{ token: '{Series Title}', example: 'The Series Title\'s!' },
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!' },
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010' },
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!' },
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' },
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The' },
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The' },
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' },
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' },
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' },
{ token: '{Series TitleFirstCharacter}', example: 'S' },
{ token: '{Series Title}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)', footNote: 1 },
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010', footNote: 1 },
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)', footNote: 1 },
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010', footNote: 1 },
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
{ token: '{Series TitleFirstCharacter}', example: 'S', footNote: 1 },
{ token: '{Series Year}', example: '2010' }
];
@@ -124,8 +124,8 @@ const absoluteTokens = [
];
const episodeTitleTokens = [
{ token: '{Episode Title}', example: 'Episode\'s Title' },
{ token: '{Episode CleanTitle}', example: 'Episodes Title' }
{ token: '{Episode Title}', example: 'Episode\'s Title', footNote: 1 },
{ token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: 1 }
];
const qualityTokens = [
@@ -149,7 +149,7 @@ const mediaInfoTokens = [
];
const otherTokens = [
{ token: '{Release Group}', example: 'Rls Grp' },
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 },
{ token: '{Custom Formats}', example: 'iNTERNAL' },
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
];
@@ -305,7 +305,7 @@ class NamingModal extends Component {
<FieldSet legend={translate('Series')}>
<div className={styles.groups}>
{
seriesTokens.map(({ token, example }) => {
seriesTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
@@ -313,6 +313,7 @@ class NamingModal extends Component {
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
@@ -322,6 +323,11 @@ class NamingModal extends Component {
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('SeriesFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('SeriesID')}>
@@ -451,7 +457,7 @@ class NamingModal extends Component {
<FieldSet legend={translate('EpisodeTitle')}>
<div className={styles.groups}>
{
episodeTitleTokens.map(({ token, example }) => {
episodeTitleTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
@@ -459,6 +465,7 @@ class NamingModal extends Component {
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
@@ -468,6 +475,10 @@ class NamingModal extends Component {
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('EpisodeTitleFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Quality')}>
@@ -523,7 +534,7 @@ class NamingModal extends Component {
<FieldSet legend={translate('Other')}>
<div className={styles.groups}>
{
otherTokens.map(({ token, example }) => {
otherTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
@@ -531,6 +542,7 @@ class NamingModal extends Component {
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
@@ -558,6 +570,11 @@ class NamingModal extends Component {
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Original')}>

View File

@@ -12,7 +12,7 @@ export default function TagInUse(props) {
return null;
}
if (count > 1 && labelPlural ) {
if (count > 1 && labelPlural) {
return (
<div>
{count} {labelPlural.toLowerCase()}

View File

@@ -1,8 +1,11 @@
import $ from 'jquery';
import _ from 'lodash';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getProviderState from 'Utilities/State/getProviderState';
import { set } from '../baseActions';
const abortCurrentRequests = {};
let lastTestData = null;
export function createCancelTestProviderHandler(section) {
return function(getState, payload, dispatch) {
@@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
return function(getState, payload, dispatch) {
dispatch(set({ section, isTesting: true }));
const testData = getProviderState(payload, getState, section);
const {
queryParams = {},
...otherPayload
} = payload;
const testData = getProviderState({ ...otherPayload }, getState, section);
const params = { ...queryParams };
// If the user is re-testing the same provider without changes
// force it to be tested.
if (_.isEqual(testData, lastTestData)) {
params.forceTest = true;
}
lastTestData = testData;
const ajaxOptions = {
url: `${url}/test`,
url: `${url}/test?${$.param(params, true)}`,
method: 'POST',
contentType: 'application/json',
dataType: 'json',
@@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
lastTestData = null;
dispatch(set({
section,
isTesting: false,

View File

@@ -1,6 +1,6 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -77,6 +77,31 @@ export const defaultState = {
isVisible: true,
isModifiable: false
}
],
selectedFilterKey: 'all',
filters: [
{
key: 'all',
label: () => translate('All'),
filters: []
}
],
filterBuilderProps: [
{
name: 'seriesIds',
label: () => translate('Series'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.SERIES
},
{
name: 'protocols',
label: () => translate('Protocol'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.PROTOCOL
}
]
};
@@ -84,6 +109,7 @@ export const persistState = [
'blocklist.pageSize',
'blocklist.sortKey',
'blocklist.sortDirection',
'blocklist.selectedFilterKey',
'blocklist.columns'
];
@@ -97,6 +123,7 @@ export const GOTO_NEXT_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistNextPage';
export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage';
export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage';
export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort';
export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter';
export const SET_BLOCKLIST_TABLE_OPTION = 'blocklist/setBlocklistTableOption';
export const REMOVE_BLOCKLIST_ITEM = 'blocklist/removeBlocklistItem';
export const REMOVE_BLOCKLIST_ITEMS = 'blocklist/removeBlocklistItems';
@@ -112,6 +139,7 @@ export const gotoBlocklistNextPage = createThunk(GOTO_NEXT_BLOCKLIST_PAGE);
export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE);
export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE);
export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT);
export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER);
export const setBlocklistTableOption = createAction(SET_BLOCKLIST_TABLE_OPTION);
export const removeBlocklistItem = createThunk(REMOVE_BLOCKLIST_ITEM);
export const removeBlocklistItems = createThunk(REMOVE_BLOCKLIST_ITEMS);
@@ -132,7 +160,8 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT,
[serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER
}),
[REMOVE_BLOCKLIST_ITEM]: createRemoveItemHandler(section, '/blocklist'),

View File

@@ -192,6 +192,22 @@ export const filterPredicates = {
});
return predicate(hasMissingSeason, filterValue);
},
hasUnmonitoredSeason: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { seasons = [] } = item;
const hasUnmonitoredSeason = seasons.some((season) => {
const {
seasonNumber,
monitored
} = season;
return seasonNumber > 0 && !monitored;
});
return predicate(hasUnmonitoredSeason, filterValue);
}
};
@@ -353,6 +369,12 @@ export const filterBuilderProps = [
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'hasUnmonitoredSeason',
label: () => translate('HasUnmonitoredSeason'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'year',
label: () => translate('Year'),

View File

@@ -2,7 +2,7 @@ import * as dark from './dark';
import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light };
const auto = defaultDark ? dark : light;
export default {
auto,

View File

@@ -2,7 +2,6 @@ module.exports = {
// Families
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
passwordFamily: 'text-security-disc',
// Sizes
extraSmallFontSize: '11px',

View File

@@ -6,6 +6,22 @@ import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
function formatTitles(titles: string[]) {
if (!titles) {
return null;
}
if (titles.length > 11) {
return (
<span title={titles.join(', ')}>
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
</span>
);
}
return <span>{titles.join(', ')}</span>;
}
export interface QueuedTaskRowNameCellProps {
commandName: string;
body: CommandBody;
@@ -32,7 +48,7 @@ export default function QueuedTaskRowNameCell(
<span className={styles.commandName}>
{commandName}
{sortedSeries.length ? (
<span> - {sortedSeries.map((s) => s.title).join(', ')}</span>
<span> - {formatTitles(sortedSeries.map((s) => s.title))}</span>
) : null}
{body.seasonNumber ? (
<span>

View File

@@ -1,43 +0,0 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
if (!date) {
return null;
}
const isTodayDate = isToday(date);
if (isTodayDate && timeForToday && timeFormat) {
return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return translate('Yesterday');
}
if (isTodayDate) {
return translate('Today');
}
if (isTomorrow(date)) {
return translate('Tomorrow');
}
if (isInNextWeek(date)) {
return moment(date).format('dddd');
}
return moment(date).format(shortDateFormat);
}
export default getRelativeDate;

View File

@@ -0,0 +1,82 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
import formatDateTime from './formatDateTime';
interface GetRelativeDateOptions {
date?: string;
shortDateFormat: string;
showRelativeDates: boolean;
timeFormat?: string;
includeSeconds?: boolean;
timeForToday?: boolean;
includeTime?: boolean;
}
function getRelativeDate({
date,
shortDateFormat,
showRelativeDates,
timeFormat,
includeSeconds = false,
timeForToday = false,
includeTime = false,
}: GetRelativeDateOptions) {
if (!date) {
return null;
}
if ((includeTime || timeForToday) && !timeFormat) {
throw new Error(
"getRelativeDate: 'timeFormat' is required when 'includeTime' or 'timeForToday' is true"
);
}
const isTodayDate = isToday(date);
const time = timeFormat
? formatTime(date, timeFormat, {
includeMinuteZero: true,
includeSeconds,
})
: '';
if (isTodayDate && timeForToday) {
return time;
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return includeTime
? translate('YesterdayAt', { time })
: translate('Yesterday');
}
if (isTodayDate) {
return includeTime ? translate('TodayAt', { time }) : translate('Today');
}
if (isTomorrow(date)) {
return includeTime
? translate('TomorrowAt', { time })
: translate('Tomorrow');
}
if (isInNextWeek(date)) {
const day = moment(date).format('dddd');
return includeTime ? translate('DayOfWeekAt', { day, time }) : day;
}
return includeTime
? formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds })
: moment(date).format(shortDateFormat);
}
export default getRelativeDate;

View File

@@ -57,8 +57,8 @@
<style>
body {
background-color: #f5f7fa;
color: #656565;
background-color: var(--pageBackground);
color: var(--textColor);
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
sans-serif;
}
@@ -88,14 +88,14 @@
padding: 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background-color: #3a3f51;
background-color: var(--themeDarkColor);
}
.panel-body {
padding: 20px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
background-color: var(--panelBackground);
}
.sign-in {
@@ -112,16 +112,18 @@
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid #dde6e9;
background-color: var(--inputBackgroundColor);
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
color: var(--textColor);
}
.form-input:focus {
outline: 0;
border-color: #66afe9;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(102, 175, 233, 0.6);
border-color: var(--inputFocusBorderColor);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
0 0 8px var(--inputFocusBoxShadowColor);
}
.button {
@@ -130,10 +132,10 @@
padding: 10px 0;
width: 100%;
border: 1px solid;
border-color: #5899eb;
border-color: var(--primaryBorderColor);
border-radius: 4px;
background-color: #5d9cec;
color: #fff;
background-color: var(--primaryBackgroundColor);
color: var(--white);
vertical-align: middle;
text-align: center;
white-space: nowrap;
@@ -141,9 +143,9 @@
}
.button:hover {
border-color: #3483e7;
background-color: #4b91ea;
color: #fff;
border-color: var(--primaryHoverBorderColor);
background-color: var(--primaryHoverBackgroundColor);
color: var(--white);
text-decoration: none;
}
@@ -165,24 +167,24 @@
.forgot-password {
margin-left: auto;
color: #909fa7;
color: var(--forgotPasswordColor);
text-decoration: none;
font-size: 13px;
}
.forgot-password:focus,
.forgot-password:hover {
color: #748690;
color: var(--forgotPasswordAltColor);
text-decoration: underline;
}
.forgot-password:visited {
color: #748690;
color: var(--forgotPasswordAltColor);
}
.login-failed {
margin-top: 20px;
color: #f05050;
color: var(--failedColor);
font-size: 14px;
}
@@ -291,5 +293,59 @@
loginFailedDiv.classList.remove("hidden");
}
var light = {
white: '#fff',
pageBackground: '#f5f7fa',
textColor: '#515253',
themeDarkColor: '#3a3f51',
panelBackground: '#fff',
inputBackgroundColor: '#fff',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#909fa7',
forgotPasswordAltColor: '#748690'
};
var dark = {
white: '#fff',
pageBackground: '#202020',
textColor: '#ccc',
themeDarkColor: '#494949',
panelBackground: '#111',
inputBackgroundColor: '#333',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#737d83',
forgotPasswordAltColor: '#546067'
};
var theme = "_THEME_";
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
dark :
light;
Object.entries(finalTheme).forEach(([key, value]) => {
document.documentElement.style.setProperty(
`--${key}`,
value
);
});
</script>
</html>

View File

@@ -0,0 +1,16 @@
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
interface Blocklist extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
title: string;
date?: string;
protocol: string;
seriesId?: number;
}
export default Blocklist;

View File

@@ -1,5 +1,5 @@
export interface UiSettings {
theme: string;
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;

View File

@@ -29,9 +29,9 @@
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.21",
"@sentry/browser": "7.100.0",
"@types/node": "18.16.8",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"@types/node": "18.19.31",
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",
"classnames": "2.3.2",
"clipboard": "2.0.11",
"connected-react-router": "6.9.3",
@@ -81,17 +81,16 @@
"redux-thunk": "2.4.2",
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "4.9.5"
"typescript": "5.1.6"
},
"devDependencies": {
"@babel/core": "7.22.11",
"@babel/eslint-parser": "7.22.11",
"@babel/plugin-proposal-export-default-from": "7.22.5",
"@babel/core": "7.24.4",
"@babel/eslint-parser": "7.24.1",
"@babel/plugin-proposal-export-default-from": "7.24.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.14",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.11",
"@types/classnames": "2.3.1",
"@babel/preset-env": "7.24.4",
"@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1",
"@types/lodash": "4.14.194",
"@types/react-lazyload": "3.2.0",
"@types/react-router-dom": "5.3.3",
@@ -99,30 +98,30 @@
"@types/react-window": "1.8.5",
"@types/redux-actions": "2.6.2",
"@types/webpack-livereload-plugin": "^2.3.3",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.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.32.1",
"core-js": "3.37.0",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.40.0",
"eslint-config-prettier": "8.8.0",
"eslint": "8.57.0",
"eslint-config-prettier": "8.10.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react": "7.34.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-simple-import-sort": "10.0.0",
"eslint-plugin-simple-import-sort": "12.1.0",
"file-loader": "6.2.0",
"filemanager-webpack-plugin": "8.0.0",
"fork-ts-checker-webpack-plugin": "8.0.0",
"html-webpack-plugin": "5.5.1",
"loader-utils": "^3.2.1",
"mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.23",
"postcss": "8.4.38",
"postcss-color-function": "4.1.0",
"postcss-loader": "7.3.0",
"postcss-mixins": "9.0.4",

View File

@@ -175,16 +175,46 @@
</Otherwise>
</Choose>
<!--
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
<Choose>
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
<PropertyGroup>
<Architecture>x64</Architecture>
</PropertyGroup>
</When>
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
<PropertyGroup>
<Architecture>x86</Architecture>
</PropertyGroup>
</When>
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
<PropertyGroup>
<Architecture>arm64</Architecture>
</PropertyGroup>
</When>
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
<PropertyGroup>
<Architecture>arm</Architecture>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup>
<Architecture></Architecture>
</PropertyGroup>
</Otherwise>
</Choose>
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
'$(RuntimeIdentifier)' == ''">
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
'$(RuntimeIdentifier)' == ''">
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(IsOSX)' == 'true' and

View File

@@ -1,10 +1,12 @@
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
using NzbDrone.Test.Common;
@@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
.Callback<string, string>((p, t) => _configFileContents = t);
Mocker.GetMock<IOptions<AuthOptions>>()
.Setup(v => v.Value)
.Returns(new AuthOptions());
Mocker.GetMock<IOptions<AppOptions>>()
.Setup(v => v.Value)
.Returns(new AppOptions());
Mocker.GetMock<IOptions<ServerOptions>>()
.Setup(v => v.Value)
.Returns(new ServerOptions());
Mocker.GetMock<IOptions<LogOptions>>()
.Setup(v => v.Value)
.Returns(new LogOptions());
Mocker.GetMock<IOptions<UpdateOptions>>()
.Setup(v => v.Value)
.Returns(new UpdateOptions());
}
[Test]

View File

@@ -18,6 +18,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
// NzbGet
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]

View File

@@ -3,6 +3,7 @@ using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Test.Common;
@@ -34,7 +35,7 @@ namespace NzbDrone.Common.Test
[TestCase(@"\\Testserver\\Test\", @"\\Testserver\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(@"\\Testserver\Test\file.ext ", @"\\Testserver\Test\file.ext")]
[TestCase(@"//CAPITAL//lower// ", @"\\CAPITAL\lower")]
public void Clean_Path_Windows(string dirty, string clean)
{
@@ -335,5 +336,30 @@ namespace NzbDrone.Common.Test
result[2].Should().Be(@"TV");
result[3].Should().Be(@"Series Title");
}
[TestCase(@"C:\Test\")]
[TestCase(@"C:\Test")]
[TestCase(@"C:\Test\TV\")]
[TestCase(@"C:\Test\TV")]
public void IsPathValid_should_be_true(string path)
{
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeTrue();
}
[TestCase(@"C:\Test \")]
[TestCase(@"C:\Test ")]
[TestCase(@"C:\ Test\")]
[TestCase(@"C:\ Test")]
[TestCase(@"C:\Test \TV")]
[TestCase(@"C:\ Test\TV")]
[TestCase(@"C:\Test \TV\")]
[TestCase(@"C:\ Test\TV\")]
[TestCase(@" C:\Test\TV\")]
[TestCase(@" C:\Test\TV")]
public void IsPathValid_should_be_false(string path)
{
path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse();
}
}
}

View File

@@ -10,6 +10,7 @@ using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
@@ -29,10 +30,16 @@ namespace NzbDrone.Common.Test
.AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddDummyDatabase()
.AddDummyLogDatabase()
.AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance(new Mock<IHostLifetime>().Object);
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
var serviceProvider = container.GetServiceProvider();

View File

@@ -29,6 +29,12 @@ namespace NzbDrone.Common.Extensions
public static string CleanFilePath(this string path)
{
if (path.IsNotNullOrWhiteSpace())
{
// Trim trailing spaces before checking if the path is valid so validation doesn't fail for something we can fix.
path = path.TrimEnd(' ');
}
Ensure.That(path, () => path).IsNotNullOrWhiteSpace();
Ensure.That(path, () => path).IsValidPath(PathValidationType.AnyOs);
@@ -37,10 +43,10 @@ namespace NzbDrone.Common.Extensions
// UNC
if (!info.FullName.Contains('/') && info.FullName.StartsWith(@"\\"))
{
return info.FullName.TrimEnd('/', '\\', ' ');
return info.FullName.TrimEnd('/', '\\');
}
return info.FullName.TrimEnd('/').Trim('\\', ' ');
return info.FullName.TrimEnd('/').Trim('\\');
}
public static bool PathNotEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
@@ -154,6 +160,23 @@ namespace NzbDrone.Common.Extensions
return false;
}
if (path.Trim() != path)
{
return false;
}
var directoryInfo = new DirectoryInfo(path);
while (directoryInfo != null)
{
if (directoryInfo.Name.Trim() != directoryInfo.Name)
{
return false;
}
directoryInfo = directoryInfo.Parent;
}
if (validationType == PathValidationType.AnyOs)
{
return IsPathValidForWindows(path) || IsPathValidForNonWindows(path);
@@ -291,6 +314,11 @@ namespace NzbDrone.Common.Extensions
return processName;
}
public static string CleanPath(this string path)
{
return Path.Join(path.Split(Path.DirectorySeparatorChar).Select(s => s.Trim()).ToArray());
}
public static string GetAppDataPath(this IAppFolderInfo appFolderInfo)
{
return appFolderInfo.AppDataFolder;

View File

@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -0,0 +1,8 @@
namespace NzbDrone.Common.Options;
public class AppOptions
{
public string InstanceName { get; set; }
public string Theme { get; set; }
public bool? LaunchBrowser { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Common.Options;
public class AuthOptions
{
public string ApiKey { get; set; }
public bool? Enabled { get; set; }
public string Method { get; set; }
public string Required { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace NzbDrone.Common.Options;
public class LogOptions
{
public string Level { get; set; }
public bool? FilterSentryEvents { get; set; }
public int? Rotate { get; set; }
public bool? Sql { get; set; }
public string ConsoleLevel { get; set; }
public bool? AnalyticsEnabled { get; set; }
public string SyslogServer { get; set; }
public int? SyslogPort { get; set; }
public string SyslogLevel { get; set; }
public bool? DbEnabled { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace NzbDrone.Common.Options;
public class ServerOptions
{
public string UrlBase { get; set; }
public string BindAddress { get; set; }
public int? Port { get; set; }
public bool? EnableSsl { get; set; }
public int? SslPort { get; set; }
public string SslCertPath { get; set; }
public string SslCertPassword { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace NzbDrone.Common.Options;
public class UpdateOptions
{
public string Mechanism { get; set; }
public bool? Automatically { get; set; }
public string ScriptPath { get; set; }
public string Branch { get; set; }
}

View File

@@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Subject.Definition.Settings.As<QBittorrentSettings>().RecentTvPriority = (int)QBittorrentPriority.First;
}
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, int maxInactiveSeedingTime = -1, QBittorrentMaxRatioAction maxRatioAction = QBittorrentMaxRatioAction.Pause)
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
@@ -118,7 +118,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
MaxRatio = maxRatio,
MaxRatioEnabled = maxRatio >= 0,
MaxSeedingTime = maxSeedingTime,
MaxSeedingTimeEnabled = maxSeedingTime >= 0
MaxSeedingTimeEnabled = maxSeedingTime >= 0,
MaxInactiveSeedingTime = maxInactiveSeedingTime,
MaxInactiveSeedingTimeEnabled = maxInactiveSeedingTime >= 0
});
}
@@ -176,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
VerifyWarning(item);
}
[Test]
public void paused_item_should_have_required_properties()
[TestCase("pausedDL")]
[TestCase("stoppedDL")]
public void paused_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
@@ -186,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedDL",
State = state,
Label = "",
SavePath = ""
};
@@ -198,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
[TestCase("queuedUP")]
[TestCase("uploading")]
[TestCase("stalledUP")]
@@ -416,8 +420,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
}
[Test]
public void api_261_should_use_content_path()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void api_261_should_use_content_path(string state)
{
var torrent = new QBittorrentTorrent
{
@@ -426,7 +431,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedUP",
State = state,
Label = "",
SavePath = @"C:\Torrents".AsOsAgnostic(),
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
@@ -610,7 +615,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
float ratio = 0.1f,
float ratioLimit = -2,
int seedingTime = 1,
int seedingTimeLimit = -2)
int seedingTimeLimit = -2,
int inactiveSeedingTimeLimit = -2,
long lastActivity = -1)
{
var torrent = new QBittorrentTorrent
{
@@ -624,7 +631,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
SavePath = "",
Ratio = ratio,
RatioLimit = ratioLimit,
SeedingTimeLimit = seedingTimeLimit
SeedingTimeLimit = seedingTimeLimit,
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit,
LastActivity = lastActivity == -1 ? DateTimeOffset.UtcNow.ToUnixTimeSeconds() : lastActivity
};
GivenTorrents(new List<QBittorrentTorrent>() { torrent });
@@ -651,44 +660,48 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
{
GivenGlobalSeedLimits(-1);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
{
GivenGlobalSeedLimits(0.2f);
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -706,33 +719,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -740,21 +756,82 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
public void should_not_be_removable_and_should_not_allow_move_files_if_max_inactive_seedingtime_reached_and_not_paused()
{
GivenGlobalSeedLimits(2.0f, 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("uploading", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_fetch_details_twice()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f, 20);
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_fetch_details_twice(string state)
{
GivenGlobalSeedLimits(-1, 30);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -766,8 +843,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]
public void should_get_category_from_the_category_if_set()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_category_if_set(string state)
{
const string category = "tv-sonarr";
GivenGlobalSeedLimits(1.0f);
@@ -779,7 +857,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
State = state,
Category = category,
SavePath = "",
Ratio = 1.0f
@@ -791,8 +869,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.Category.Should().Be(category);
}
[Test]
public void should_get_category_from_the_label_if_the_category_is_not_available()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
{
const string category = "tv-sonarr";
GivenGlobalSeedLimits(1.0f);
@@ -804,7 +883,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
State = state,
Label = category,
SavePath = "",
Ratio = 1.0f

View File

@@ -1,6 +1,9 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.AutoTagging;
using NzbDrone.Core.AutoTagging.Specifications;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Core.Tags;
@@ -45,5 +48,35 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_not_delete_used_auto_tagging_tag_specification_tags()
{
var tags = Builder<Tag>
.CreateListOfSize(2)
.All()
.With(x => x.Id = 0)
.BuildList();
Db.InsertMany(tags);
var autoTags = Builder<AutoTag>.CreateListOfSize(1)
.All()
.With(x => x.Id = 0)
.With(x => x.Specifications = new List<IAutoTaggingSpecification>
{
new TagSpecification
{
Name = "Test",
Value = tags[0].Id
}
})
.BuildList();
Mocker.GetMock<IAutoTaggingRepository>().Setup(s => s.All())
.Returns(autoTags);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
}
}

View File

@@ -608,5 +608,47 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2);
allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3);
}
[Test]
public async Task episode_search_should_include_series_title_when_not_a_direct_title_match()
{
_xemSeries.Title = "Sonarr's Title";
_xemSeries.CleanTitle = "sonarrstitle";
WithEpisode(1, 12, 2, 3);
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.FindByTvdbId(It.IsAny<int>()))
.Returns(new List<SceneMapping>
{
new SceneMapping
{
TvdbId = _xemSeries.TvdbId,
SearchTerm = "Sonarrs Title",
ParseTerm = _xemSeries.CleanTitle,
SeasonNumber = 1,
SceneSeasonNumber = 1,
SceneOrigin = "tvdb",
Type = "ServicesProvider"
}
});
var allCriteria = WatchForSearchCriteria();
await Subject.EpisodeSearch(_xemEpisodes.First(), false, false);
Mocker.GetMock<ISceneMappingService>()
.Verify(v => v.FindByTvdbId(_xemSeries.Id), Times.Once());
allCriteria.Should().HaveCount(2);
allCriteria.First().Should().BeOfType<SingleEpisodeSearchCriteria>();
allCriteria.First().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(1);
allCriteria.First().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(12);
allCriteria.Last().Should().BeOfType<SingleEpisodeSearchCriteria>();
allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2);
allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
@@ -12,5 +13,7 @@ namespace NzbDrone.Core.Test.IndexerTests
}
public string BaseUrl { get; set; }
public IEnumerable<int> MultiLanguages { get; set; }
}
}

View File

@@ -9,13 +9,14 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
[TestFixture]
public class AggregateSubtitleInfoFixture : CoreTest<AggregateSubtitleInfo>
{
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")]
public void should_do_basic_parse(string relativePath, string originalFilePath, string path)
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)]
[TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 5.1].mkv", "", "Name (2020) - S01E20 - [FLAC 2.0].fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [FLAC 2.0].mkv")]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)]
[TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)]
public void should_do_basic_parse(string relativePath, string originalFilePath, string path, string fileNameBeforeRename)
{
var episodeFile = new EpisodeFile
{
@@ -23,7 +24,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
OriginalFilePath = originalFilePath
};
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, fileNameBeforeRename);
subtitleTitleInfo.Title.Should().BeNull();
subtitleTitleInfo.Copy.Should().Be(0);
@@ -40,7 +41,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
RelativePath = relativePath
};
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path);
var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, null);
subtitleTitleInfo.LanguageTags.Should().NotContain("default");
}

View File

@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
[TestCase(10, "", "", "", null, HdrFormat.None)]
[TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)]
[TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)]
[TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.None)]
[TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)]

View File

@@ -5,7 +5,6 @@ using FluentValidation.Results;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation;
using NzbDrone.Test.Common;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Test.NotificationTests
[TestFixture]
public class NotificationBaseFixture : TestBase
{
private class TestSetting : IProviderConfig
private class TestSetting : NotificationSettingsBase<TestSetting>
{
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult();
}

View File

@@ -134,6 +134,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)]
[TestCase("[Mystic Z-Team] Series Title Super - Episode 013 VF - Non-censuré [720p].mp4", "Series Title Super", 13, 0, 0)]
[TestCase("Series Title Kai Episodio 13 Audio Latino", "Series Title Kai", 13, 0, 0)]
[TestCase("Series_Title_2_[01]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 0, 0)]
// [TestCase("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
@@ -179,6 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Erai-raws] Series-Title! 2 - 01~10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)]
[TestCase("[Erai-raws] Series Title! - 01 ~ 10 [1080p][Multiple Subtitle]", "Series Title!", 1, 10)]
[TestCase("[Erai-raws] Series-Title! 2 - 01 ~ 10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)]
[TestCase("Series_Title_2_[01-05]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 5)]
// [TestCase("", "", 1, 2)]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber)

View File

@@ -33,6 +33,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("2019_08_20_1080_all.mp4", "", 2019, 8, 20)]
[TestCase("Series and Title 20201013 Ep7432 [720p WebRip (x264)] [SUBS]", "Series and Title", 2020, 10, 13)]
[TestCase("Series Title (1955) - 1954-01-23 05 00 00 - Cottage for Sale.ts", "Series Title (1955)", 1954, 1, 23)]
[TestCase("Series Title - 30-04-2024 HDTV 1080p H264 AAC", "Series Title", 2024, 4, 30)]
// [TestCase("", "", 0, 0, 0)]
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
@@ -100,5 +101,13 @@ namespace NzbDrone.Core.Test.ParserTests
Parser.Parser.ParseTitle(title).Should().BeNull();
}
[TestCase("Tmc - Quotidien - 05-06-2024 HDTV 1080p H264 AAC")]
// [TestCase("", "", 0, 0, 0)]
public void should_not_parse_ambiguous_daily_episode(string postTitle)
{
Parser.Parser.ParseTitle(postTitle).Should().BeNull();
}
}
}

View File

@@ -429,6 +429,26 @@ namespace NzbDrone.Core.Test.ParserTests
result.Languages.Should().Contain(Language.English);
}
[TestCase("Остання серія (Сезон 1) / The Last Series (Season 1) (2024) WEB-DLRip-AVC 2xUkr/Eng | Sub Ukr/Eng")]
[TestCase("Справжня серія (Сезон 1-3) / True Series (Season 1-3) (2014-2019) BDRip-AVC 3xUkr/Eng | Ukr/Eng")]
[TestCase("Серія (Сезон 1-3) / The Series (Seasons 1-3) (2019-2022) BDRip-AVC 4xUkr/Eng | Sub 2xUkr/Eng")]
public void should_parse_english_and_ukranian(string postTitle)
{
var result = Parser.Parser.ParseTitle(postTitle);
result.Languages.Count.Should().Be(2);
result.Languages.Should().Contain(Language.Ukrainian);
result.Languages.Should().Contain(Language.English);
}
[TestCase("Серія (Сезон 1, серії 01-26 із 51) / Seri (Season 1, episodes 01-26) (2018) WEBRip-AVC 2Ukr/Tur")]
public void should_parse_turkish_and_ukranian(string postTitle)
{
var result = Parser.Parser.ParseTitle(postTitle);
result.Languages.Count.Should().Be(2);
result.Languages.Should().Contain(Language.Ukrainian);
result.Languages.Should().Contain(Language.Turkish);
}
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
@@ -444,6 +464,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ru-something-else.srt", new string[0], "something-else", "Russian")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].Full Subtitles.eng.ass", new string[0], "Full Subtitles", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle - 1.en.ass", new string[0], "mytitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle 1.en.ass", new string[0], "mytitle 1", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle.en.ass", new string[0], "mytitle", "English")]
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
{

View File

@@ -31,6 +31,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase(@"C:\Test\Series\Season 1\2 Honor Thy Developer (1080p HD).m4v", 1, 2)]
[TestCase(@"C:\Test\Series\Season 2 - Total Series Action\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
[TestCase(@"C:\Test\Series\Season 2\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)]
[TestCase(@"C:\Test\Series\Season 1\02.04.24 - S01E01 - The Rabbit Hole", 1, 1)]
[TestCase(@"C:\Test\Series\Season 1\8 Series Rules - S01E01 - Pilot", 1, 1)]
// [TestCase(@"C:\series.state.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime); 2020-01 broken test case: Expected result.EpisodeNumbers to contain 1 item(s), but found 0
public void should_parse_from_path(string path, int season, int episode)

View File

@@ -109,6 +109,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Series.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)]
[TestCase("The Series (BD)(640x480(RAW) (BATCH 1) (1-13)", false)]
[TestCase("[Doki] Series - 02 (848x480 XviD BD MP3) [95360783]", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01.BluRay.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray480p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray480p, proper);
@@ -309,6 +311,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Sans.Series.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)]
[TestCase("Series.Black.1x01.Selezione.Naturale.ITA.720p.BDMux.x264-NovaRip", false)]
[TestCase("Series.Hunter.S02.720p.Blu-ray.Remux.AVC.FLAC.2.0-SiCFoI", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.720p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
@@ -340,6 +343,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S03E01.The.Calm.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR", false)]
[TestCase("Series Title Season 2 (BDRemux 1080p HEVC FLAC) [Netaro]", false)]
[TestCase("[Vodes] Series Title - Other Title (2020) [BDRemux 1080p HEVC Dual-Audio]", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.1080p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray1080p_remux_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray1080pRemux, proper);
@@ -360,6 +364,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S01E08.The.Sonarr.BluRay.2160p.AVC.DTS-HD.MA.5.1.REMUX-FraMeSToR", false)]
[TestCase("Series.Title.2x11.Nato.Per.The.Sonarr.Bluray.Remux.AVC.2160p.AC3.ITA", false)]
[TestCase("[Dolby Vision] Sonarr.of.Series.S07.MULTi.UHD.BLURAY.REMUX.DV-NoTag", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.2160p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray2160p_remux_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray2160pRemux, proper);

View File

@@ -85,6 +85,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")]
[TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")]
[TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")]
[TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")]
[TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")]
public void should_parse_exception_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);

View File

@@ -31,6 +31,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Stagione.3.HDTV.XviD-NOTAG", "Series", 3)]
[TestCase("Series.Stagione.3.HDTV.XviD-NOTAG", "Series", 3)]
[TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)]
[TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)]
[TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)]
public void should_parse_full_season_release(string postTitle, string title, int season)
{
var result = Parser.Parser.ParseTitle(postTitle);
@@ -73,6 +75,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Series.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series 2016", 2, 1)]
[TestCase("The.Series.S07.Vol.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series", 7, 1)]
[TestCase("The.Series.S06.P1.1080p.Blu-Ray.10-Bit.Dual-Audio.TrueHD.x265-iAHD", "The Series", 6, 1)]
public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -85,7 +85,8 @@ namespace NzbDrone.Core.Annotations
Device,
TagSelect,
RootFolder,
QualityProfile
QualityProfile,
SeriesTag
}
public enum HiddenType

View File

@@ -0,0 +1,36 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.AutoTagging.Specifications
{
public class TagSpecificationValidator : AbstractValidator<TagSpecification>
{
public TagSpecificationValidator()
{
RuleFor(c => c.Value).GreaterThan(0);
}
}
public class TagSpecification : AutoTaggingSpecificationBase
{
private static readonly TagSpecificationValidator Validator = new ();
public override int Order => 1;
public override string ImplementationName => "Tag";
[FieldDefinition(1, Label = "AutoTaggingSpecificationTag", Type = FieldType.SeriesTag)]
public int Value { get; set; }
protected override bool IsSatisfiedByWithoutNegate(Series series)
{
return series.Tags.Contains(Value);
}
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@@ -40,11 +40,29 @@ namespace NzbDrone.Core.Blocklisting
Delete(x => seriesIds.Contains(x.SeriesId));
}
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType).Join<Blocklist, Series>((b, m) => b.SeriesId == m.Id);
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder sql) => _database.QueryJoined<Blocklist, Series>(sql, (bl, movie) =>
{
bl.Series = movie;
return bl;
});
public override PagingSpec<Blocklist> GetPaged(PagingSpec<Blocklist> pagingSpec)
{
pagingSpec.Records = GetPagedRecords(PagedBuilder(), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Blocklist))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder().Select(typeof(Blocklist)), pagingSpec, countTemplate);
return pagingSpec;
}
protected override SqlBuilder PagedBuilder()
{
var builder = Builder()
.Join<Blocklist, Series>((b, m) => b.SeriesId == m.Id);
return builder;
}
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) =>
_database.QueryJoined<Blocklist, Series>(builder, (blocklist, series) =>
{
blocklist.Series = series;
return blocklist;
});
}
}

View File

@@ -10,6 +10,7 @@ using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Datastore;
@@ -53,6 +54,7 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; }
int SyslogPort { get; }
string SyslogLevel { get; }
bool LogDbEnabled { get; }
string Theme { get; }
string PostgresHost { get; }
int PostgresPort { get; }
@@ -70,6 +72,11 @@ namespace NzbDrone.Core.Configuration
private readonly IDiskProvider _diskProvider;
private readonly ICached<string> _cache;
private readonly PostgresOptions _postgresOptions;
private readonly AuthOptions _authOptions;
private readonly AppOptions _appOptions;
private readonly ServerOptions _serverOptions;
private readonly UpdateOptions _updateOptions;
private readonly LogOptions _logOptions;
private readonly string _configFile;
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -80,13 +87,23 @@ namespace NzbDrone.Core.Configuration
ICacheManager cacheManager,
IEventAggregator eventAggregator,
IDiskProvider diskProvider,
IOptions<PostgresOptions> postgresOptions)
IOptions<PostgresOptions> postgresOptions,
IOptions<AuthOptions> authOptions,
IOptions<AppOptions> appOptions,
IOptions<ServerOptions> serverOptions,
IOptions<UpdateOptions> updateOptions,
IOptions<LogOptions> logOptions)
{
_cache = cacheManager.GetCache<string>(GetType());
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_configFile = appFolderInfo.GetConfigPath();
_postgresOptions = postgresOptions.Value;
_authOptions = authOptions.Value;
_appOptions = appOptions.Value;
_serverOptions = serverOptions.Value;
_updateOptions = updateOptions.Value;
_logOptions = logOptions.Value;
}
public Dictionary<string, object> GetConfigDictionary()
@@ -142,7 +159,7 @@ namespace NzbDrone.Core.Configuration
{
const string defaultValue = "*";
var bindAddress = GetValue("BindAddress", defaultValue);
var bindAddress = _serverOptions.BindAddress ?? GetValue("BindAddress", defaultValue);
if (string.IsNullOrWhiteSpace(bindAddress))
{
return defaultValue;
@@ -152,19 +169,19 @@ namespace NzbDrone.Core.Configuration
}
}
public int Port => GetValueInt("Port", 8989);
public int Port => _serverOptions.Port ?? GetValueInt("Port", 8989);
public int SslPort => GetValueInt("SslPort", 9898);
public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", 9898);
public bool EnableSsl => GetValueBoolean("EnableSsl", false);
public bool EnableSsl => _serverOptions.EnableSsl ?? GetValueBoolean("EnableSsl", false);
public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true);
public bool LaunchBrowser => _appOptions.LaunchBrowser ?? GetValueBoolean("LaunchBrowser", true);
public string ApiKey
{
get
{
var apiKey = GetValue("ApiKey", GenerateApiKey());
var apiKey = _authOptions.ApiKey ?? GetValue("ApiKey", GenerateApiKey());
if (apiKey.IsNullOrWhiteSpace())
{
@@ -180,7 +197,7 @@ namespace NzbDrone.Core.Configuration
{
get
{
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
var enabled = _authOptions.Enabled ?? GetValueBoolean("AuthenticationEnabled", false, false);
if (enabled)
{
@@ -188,20 +205,25 @@ namespace NzbDrone.Core.Configuration
return AuthenticationType.Basic;
}
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
? enumValue
: GetValueEnum("AuthenticationMethod", AuthenticationType.None);
}
}
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public AuthenticationRequiredType AuthenticationRequired =>
Enum.TryParse<AuthenticationRequiredType>(_authOptions.Required, out var enumValue)
? enumValue
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false);
public string Branch => GetValue("Branch", "main").ToLowerInvariant();
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string LogLevel => _logOptions.Level ?? GetValue("LogLevel", "info").ToLowerInvariant();
public string ConsoleLogLevel => _logOptions.ConsoleLevel ?? GetValue("ConsoleLogLevel", string.Empty, persist: false);
public string Theme => GetValue("Theme", "auto", persist: false);
public string Theme => _appOptions.Theme ?? GetValue("Theme", "auto", persist: false);
public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false);
public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false);
@@ -209,18 +231,18 @@ namespace NzbDrone.Core.Configuration
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "sonarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "sonarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertPath => GetValue("SslCertPath", "");
public string SslCertPassword => GetValue("SslCertPassword", "");
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertPath => _serverOptions.SslCertPath ?? GetValue("SslCertPath", "");
public string SslCertPassword => _serverOptions.SslCertPassword ?? GetValue("SslCertPassword", "");
public string UrlBase
{
get
{
var urlBase = GetValue("UrlBase", "").Trim('/');
var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/');
if (urlBase.IsNullOrWhiteSpace())
{
@@ -237,7 +259,7 @@ namespace NzbDrone.Core.Configuration
{
get
{
var instanceName = GetValue("InstanceName", BuildInfo.AppName);
var instanceName = _appOptions.InstanceName ?? GetValue("InstanceName", BuildInfo.AppName);
if (instanceName.StartsWith(BuildInfo.AppName) || instanceName.EndsWith(BuildInfo.AppName))
{
@@ -248,17 +270,20 @@ namespace NzbDrone.Core.Configuration
}
}
public bool UpdateAutomatically => GetValueBoolean("UpdateAutomatically", false, false);
public bool UpdateAutomatically => _updateOptions.Automatically ?? GetValueBoolean("UpdateAutomatically", false, false);
public UpdateMechanism UpdateMechanism => GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
public UpdateMechanism UpdateMechanism =>
Enum.TryParse<UpdateMechanism>(_updateOptions.Mechanism, out var enumValue)
? enumValue
: GetValueEnum("UpdateMechanism", UpdateMechanism.BuiltIn, false);
public string UpdateScriptPath => GetValue("UpdateScriptPath", "", false);
public string UpdateScriptPath => _updateOptions.ScriptPath ?? GetValue("UpdateScriptPath", "", false);
public string SyslogServer => GetValue("SyslogServer", "", persist: false);
public string SyslogServer => _logOptions.SyslogServer ?? GetValue("SyslogServer", "", persist: false);
public int SyslogPort => GetValueInt("SyslogPort", 514, persist: false);
public int SyslogPort => _logOptions.SyslogPort ?? GetValueInt("SyslogPort", 514, persist: false);
public string SyslogLevel => GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
public int GetValueInt(string key, int defaultValue, bool persist = true)
{
@@ -394,13 +419,21 @@ namespace NzbDrone.Core.Configuration
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it.");
}
return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList();
if (config.Count != 1)
{
throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Sonarr will recreate it.");
}
return xDoc;
}
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
return xDoc;
return newXDoc;
}
}
catch (XmlException ex)

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