1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

100 Commits

Author SHA1 Message Date
Taloth Saldono
a200dd5f6d Bump package version 2021-03-27 22:14:03 +01:00
Taloth Saldono
f57efd30b8 Fixed data dir ownership in case of dpkg-reconfigure 2021-03-27 21:30:30 +01:00
bakerboy448
f2f1039c5e Fixed: Debatable typos in Naming Modal 2021-03-27 13:30:22 -07:00
bakerboy448
12ba4f73ed Updating the bug template yet again 2021-03-26 19:14:08 -07:00
Mark McDowall
370280b4bf Another wiki URL update
Close #4411
2021-03-26 19:08:04 -07:00
Mark McDowall
6c505937da Fixed: Interactive import modal horizontal scrolling on Firefox mobile
Closes #4401
2021-03-23 20:37:13 -07:00
Taloth Saldono
7272c5b7fc Added IsTorrentLoaded to tests 2021-03-20 00:33:51 +01:00
Mark McDowall
1d06c3fc15 Revert vswhere command 2021-03-19 15:21:04 -07:00
Mark McDowall
ec9f62285a Updated vswhere.exe 2021-03-19 14:40:17 -07:00
Mark McDowall
aae0d1c4ba Updated nuget.exe 2021-03-19 14:25:59 -07:00
Taloth Saldono
652d44722b Fixed: Qbittorrent api errors when only one of two seed criteria was configured
closes #4393
2021-03-19 21:32:42 +01:00
Taloth Saldono
5a69801877 Fixed: Unnecessary idle cpu usage
ref #4386
2021-03-19 02:48:09 +01:00
Taloth Saldono
fa8b2f48e7 Don't ignore original wal/journal during v3 migration 2021-03-18 23:43:16 +01:00
Taloth Saldono
34faa417c1 Fixed: Database migration failure when database was manually repaired in a certain way
fixes #4390
2021-03-18 01:25:21 +01:00
Mark McDowall
d6c0635a26 New: Improve message if Sonarr can't bind to IP/port during startup
Closes #4352
2021-03-17 17:21:30 -07:00
Mark McDowall
d4167d7169 New: Support for using parsed season number for some anime releases without aliases
Closes #4377
2021-03-17 17:14:54 -07:00
Taloth Saldono
eea6be459d Fixed post-install update check not running 2021-03-14 20:24:08 +01:00
Taloth Saldono
67e97f7aee Fixed: Setting seed criteria while torrent is still being loaded by qbittorrent
closes #4360
2021-03-14 00:46:28 +01:00
Mark McDowall
37e1c4f2eb New: Don't close interactive search with background click 2021-03-11 08:23:06 -08:00
Taloth Saldono
a9b8ec3505 Fixed failing tests 2021-03-10 23:38:59 +01:00
Taloth Saldono
f57cf1561b Log Skyhook connection failures with more info. 2021-03-10 23:05:19 +01:00
Taloth Saldono
6672650b6b Log Skyhook connection failures with more info. 2021-03-10 21:44:32 +01:00
Taloth Saldono
e4a064a1c0 Added comment to sonarr.service 2021-03-10 21:44:32 +01:00
Taloth Saldono
a848e575cd Make it clearer that Maximum size is the global limit. 2021-03-10 21:44:32 +01:00
Taloth Saldono
01995e686d New: Multiple Recipients on Email Notifications (Also CC, BCC)
Based on Qstick's Radarr commit of the same name
closes #4369

Signed-off-by: Taloth Saldono <Taloth@users.noreply.github.com>
2021-03-10 21:44:31 +01:00
Taloth Saldono
32058f1705 Fixed systemd unit search&replace issue and added umask to debconf 2021-03-10 21:44:31 +01:00
Mark McDowall
af3696af08 On Download -> On Import (again) 2021-03-10 11:25:29 -08:00
Mark McDowall
1477356cfc Update Discord link 2021-03-08 19:29:11 -08:00
Mark McDowall
aa19ddfbfd Fixed: Parsing of absolute episode numbers over 1000
Closes #4367
2021-03-08 19:22:41 -08:00
Mark McDowall
a697a69e88 Fixed: Some health check wiki links 2021-03-08 19:22:34 -08:00
Mark McDowall
3abb7e156a Fixed: Parsing of absolute episode number inside square brackets
Closes #4331
2021-03-07 17:31:43 -08:00
Mark McDowall
240791a7cd Fixed: Parsing of anime batch releases using a tilde instead of a dash
Closes #4330
2021-03-07 17:10:00 -08:00
Mark McDowall
0fe2453962 Fixed: Parsing similar series titles with common words at end 2021-03-07 16:53:56 -08:00
Robin Dadswell
85f4cbe94c Fix: Consistent SSL option for Download Clients
Closes #4323
2021-03-07 16:32:19 -08:00
Mark McDowall
e1f7bce14b New: Simplify Connection trigger settings
Closes #4351
2021-03-07 16:24:20 -08:00
Mark McDowall
675c72f02e Fixed: Set SameSite=Strict for SonarrAuth cookie
Closes #4365
2021-03-07 16:24:20 -08:00
Mark McDowall
6619350f87 Fixed: Don't set cookies for static resources
Closes #4356
2021-03-07 16:24:20 -08:00
Mark McDowall
efd9fe9ad0 Fixed: Cache headers for static resources
Towards #4356
2021-03-07 16:24:20 -08:00
Mark McDowall
ab502ffda4 Just one Application Version header 2021-03-07 16:24:20 -08:00
Taloth Saldono
90697d77a5 Bumped package version for main 2021-03-07 23:27:22 +01:00
Qstick
fa7aa05d60 Cleanup formatting in PackageGlobalMessageCheck.cs 2021-03-07 13:26:19 -08:00
Mark McDowall
4ed5fefcc6 Fixed: Remove selected in queue
Closes #4364
2021-03-07 12:24:06 -08:00
Taloth Saldono
4c324fbbbf Fixed failing test 2021-03-07 00:00:41 +01:00
Taloth Saldono
7da02c236a Added mechanism for package maintainers to produce a health check error. 2021-03-06 22:47:15 +01:00
Taloth Saldono
79cfa3a5f6 Removed bad file from commit 2021-03-06 22:47:15 +01:00
Qstick
2746556ae2 Cleanup trailing space in HttpResponse 2021-03-06 17:49:14 +01:00
Taloth Saldono
d668e923af New: Allow user to choose whether delay profile should apply to release of the highest enabled quality 2021-03-06 13:57:11 +01:00
Taloth Saldono
24ca47356e Sentry logging exceptions and some trace logging 2021-03-06 13:57:11 +01:00
Taloth Saldono
ab4f57f2fa Debug logging for email notifications
ref #4348
2021-03-06 13:57:10 +01:00
Mark McDowall
13ff2d4c70 Fixed: Restoring a backup with a different API didn't reload properly 2021-03-05 18:29:00 -08:00
Mark McDowall
2728bf79ca New: Improve messaging if release is in queue because all episodes in release were not imported 2021-03-05 18:29:00 -08:00
Mark McDowall
cd28af98ee Fixed: Removal of previous service 2021-03-05 18:28:59 -08:00
Mark McDowall
e9818b9982 Fixed: Queue refresh closing manual import from queue if items change 2021-03-05 18:28:59 -08:00
Qstick
d6cf370bcd Handle 303 and 307 redirects in Http Requests 2021-03-03 20:43:44 -08:00
Michael Casey
cb8ed74fe9 New: Add Recommended to the List types for Trakt
Closes #4167
2021-02-27 12:18:07 -08:00
bakerboy448
4e81b33006 Update contributing.md Github docs URL 2021-02-27 12:16:38 -08:00
bakerboy448
e67864fecb Fixed: Cleanse Tracker Announce Keys from logs
Closes #4341
2021-02-25 00:22:58 +01:00
Taloth Saldono
e289c428c6 Fixed: Refresh scene naming exceptions on series add to help first-use scenario 2021-02-20 20:04:34 +01:00
Taloth Saldono
23047623ee Cleanse more /home/username scenarios 2021-02-20 20:04:34 +01:00
Mark McDowall
062e47e27e Fixed: History details incorrect when preferred word score was 0
Closes #4328
2021-02-16 22:35:11 -08:00
Taloth Saldono
28ba037630 Fixed: Searching specials with NNTMux-based usenet indexers 2021-02-16 21:57:53 +01:00
Taloth Saldono
82da38941e Fixed: Debian package dependencies
closes #4332
2021-02-16 21:57:53 +01:00
Mark McDowall
10c770b116 Fixed: Use original file path when calculating preferred word score for existing file
Closes #3488
Closes #3913
2021-02-13 17:13:09 -08:00
Mark McDowall
3c45349404 New: Include renamed file information for Webhook and Custom Scripts
Closes #3927
2021-02-13 17:13:09 -08:00
Mark McDowall
b815d27a10 New: Include episode file with episode file deleted events
Closes #4282
2021-02-12 17:01:31 -08:00
Mark McDowall
ec698c2cf7 Fixed: Parsing of release names with trailing colon in the title
Closes #4238
2021-02-11 17:00:11 -08:00
Mark McDowall
e7d57a95f2 Series editor column fixes
Fixed: Series Editor sorting by size on disk
Fixed: Series Editor column order/enabled lost on refresh
2021-02-10 16:52:21 -08:00
Mark McDowall
1250d71e80 Appeasing the lint gods 2021-02-09 20:17:53 -08:00
Mark McDowall
e42d1af5ff Fixed: Unable to close indexer category select input on mobile
Closes #4296
2021-02-09 17:35:32 -08:00
Mark McDowall
88ad6f9544 Fixed: Error checking if files should be deleted after import won't leave import in limbo
Closes #4318
2021-02-09 17:35:32 -08:00
Michael Casey
54c386dd22 Use SVG for loading page icon
closes #4311
2021-02-09 19:20:59 +01:00
Mark McDowall
694360457d Fixed: Error logged when notification fails to send after episode file is deleted
Closes #4289
2021-02-09 07:55:25 -08:00
Mark McDowall
ae196af2ad New: Health check for import lists with missing root folders
New: Show missing root folder path in edit for Import List

Closes #4315
2021-02-08 23:12:23 -08:00
Mark McDowall
12fafb2457 Fixed: Mark as Failed errors 2021-02-08 19:26:06 -08:00
Mark McDowall
795bc91d25 Fixed: Error logged when notification fails to send after episode file is deleted
Closes #4289
2021-02-08 16:39:47 -08:00
Mark McDowall
044342f677 Fixed: Scene name not being set during import 2021-02-08 11:10:42 -08:00
Mark McDowall
5960035d5d Fixed: Restoring backup from zip file on disk 2021-02-08 08:21:14 -08:00
Mark McDowall
6c324c8a1c Fixed: Errors loading queue after episodes in series are removed
Closes #3565
2021-02-07 20:25:44 -08:00
Mark McDowall
54a267d860 Fixed: Don't automatically import if absolutely numbered file if it doesn't match expected season
Closes #377
2021-02-07 17:28:16 -08:00
Mark McDowall
b5f08a8f06 Alternate titles prop validation 2021-02-07 16:52:57 -08:00
Mark McDowall
653db8290e Update column properties when restoring persisted state 2021-02-07 16:52:57 -08:00
Mark McDowall
cbc4295f28 Fixed: Use file name when importing batch release when renaming is disabled
Closes #3056
2021-02-07 16:52:57 -08:00
Mark McDowall
8876c9194d New: Show preferred word score in history 2021-02-07 16:52:56 -08:00
Taloth Saldono
d898f55660 Generalized RateLimit logic to all indexers based on indexer id 2021-02-08 00:09:59 +01:00
Taloth Saldono
a85979c2f6 New: Added Hindi language
closes #4275
2021-02-07 21:35:30 +01:00
bakerboy448
ae63373b2b Update parser tests to be generic 2021-02-07 21:06:41 +01:00
Mark McDowall
044cb563a6 Fixed: Table column order resetting after refresh
#4297
2021-02-07 11:52:44 -08:00
Michael Casey
5302ee05bc New: Add logo to loading page
Closes #4304
2021-02-07 11:51:53 -08:00
Taloth Saldono
29bc660cfb Fixed: Jackett indexer search performance 2021-02-07 19:50:04 +01:00
Taloth Saldono
f8b8afdaa2 New: Added Arabic language 2021-02-07 19:50:04 +01:00
Ricardo Loureiro
33b708927c Fixed: Authentication on DSM 7
closes #3943
ref #4313
2021-02-07 14:05:31 +01:00
Mark McDowall
42d9e37e7d Fixed: Settings fields being altered during save
Closes #4309
2021-02-06 19:28:41 -08:00
Michael Casey
fc3bea370f New: Persist search settings in add new series
Closes #4245
2021-02-06 19:18:47 -08:00
Robin Dadswell
a1ddcf2b7b New: Show number of files as tooltip over size on disk
Closes #4203
2021-02-06 19:15:31 -08:00
bakerboy448
5b98a17873 Update feature request template 2021-02-06 19:06:53 -08:00
Mark McDowall
8fd4adbdb6 New: use-credentials for maniftest requests
Closes #4305
2021-02-04 20:28:24 -08:00
Mark McDowall
952a7248c9 New: Add FileId to History data for import events 2021-02-04 20:28:24 -08:00
bakerboy448
577604fccc Fixed: Series Removed From TVDB wiki link 2021-02-03 20:50:43 -08:00
Matt Evans
3d3cd8cf5c Detect Dolby Vision as HDR and MediaInfo Update
Fixed: Detect Dolby Vision as HDR
New: Updated MediaInfo on Windows and MacOS
Closes #4276
2021-02-02 17:19:22 -08:00
Robert Dailey
5e4c9dfe60 New: Add name field to release profiles 2021-02-02 16:51:06 -08:00
236 changed files with 3867 additions and 1683 deletions

View File

@@ -2,7 +2,7 @@
name: Bug Report
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!
title: ''
labels: 'bug'
labels: ''
assignees: ''
---
@@ -32,5 +32,6 @@ assignees: ''
- Sonarr Branch: <!--[e.g. master, develop , phantom-develop]-->
**Trace Logs**
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
<!-- Trace logs are named Sonarr.trace.txt or Sonarr.trace.#.txt and will contain "trace" in them-->

View File

@@ -1,14 +1,20 @@
---
name: Feature request
about: Suggest an idea for Sonarr
title: ''
labels: ''
assignees: ''
---
**Describe the problem**
A clear and concise description of the problem you're looking to solve.
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe any solutions you think might work**
A clear and concise description of any solutions or features you've considered.
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
Add any other context or screenshots about the feature request here.
<!-- Add any other context or screenshots about the feature request here. -->

View File

@@ -17,7 +17,7 @@ Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information w
### Getting started ###
1. Fork Sonarr
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
3. Install the required Node Packages `yarn install`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86`

View File

@@ -1,4 +1,5 @@
fromdos ./debian/*
chmod ugo-x ./debian/*
cp -r ./debian ./debian_backup
BuildVersion=${dependent_build_number:-3.10.0.999}

View File

@@ -1 +1 @@
8
10

View File

@@ -8,7 +8,10 @@ db_input high sonarr/owning_group || true
db_endblock
db_go
db_beginblock
db_input low sonarr/owning_umask || true
db_input low sonarr/config_directory || true
db_endblock
db_go
exit 0

View File

@@ -9,6 +9,8 @@ db_get sonarr/owning_user
USER="$RET"
db_get sonarr/owning_group
GROUP="$RET"
db_get sonarr/owning_umask
UMASK="$RET"
db_get sonarr/config_directory
CONFDIR="$RET"
@@ -64,9 +66,11 @@ fi
# Create data directory
if [ ! -d "$CONFDIR" ]; then
mkdir -p "$CONFDIR"
chown -R $USER:$GROUP "$CONFDIR"
fi
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
chown -R $USER:$GROUP "$CONFDIR"
#BEGIN BUILTIN UPDATER
# Apply patch if present
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
@@ -92,7 +96,7 @@ fi
chown -R $USER:$GROUP /usr/lib/sonarr
# Update sonarr.service file
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ "$UPDATER" = "BuiltIn" ]; then

View File

@@ -1,2 +1,3 @@
ignores msbuild
ignores libmediainfo0v5
ignores libc6

View File

@@ -1,3 +1,6 @@
# This file is owned by the sonarr package, DO NOT MODIFY MANUALLY
# Instead use 'dpkg-reconfigure -plow sonarr' to modify User/Group/UMask/-data
# Or use systemd built-in override functionality using 'systemctl edit sonarr'
[Unit]
Description=Sonarr Daemon
After=network.target

View File

@@ -14,6 +14,12 @@ Description: Sonarr group:
Any media files created by Sonarr will be writeable by this group.
It's advisable to keep the group the same between download client, Sonarr and media centers.
Template: sonarr/owning_umask
Type: string
Default: 0002
Description: Sonarr umask:
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
Template: sonarr/config_directory
Type: string
Default: /var/lib/sonarr

View File

@@ -1,16 +1,25 @@
FROM ubuntu:xenial AS builder
FROM ubuntu:focal AS builder
ENV DEBIAN_FRONTEND noninteractive
ENV MONO_VERSION 5.18
RUN apt-get update && \
apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
apt-transport-https \
wget dirmngr gpg gpg-agent \
# add-apt-repository for PPAs
software-properties-common && \
rm -rf /var/lib/apt/lists/*
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \
echo "deb http://download.mono-project.com/repo/debian stable-focal main" > /etc/apt/sources.list.d/mono-official-stable.list && \
apt-get update && apt-get install -y \
devscripts build-essential tofrodos \
dh-make dh-systemd \
cli-common-dev \
mono-complete \
sqlite3 libcurl3 mediainfo
sqlite3 libcurl4 mediainfo
RUN apt-get upgrade -y
RUN apt-cache policy mono-complete
RUN apt-cache policy cli-common-dev

View File

@@ -63,9 +63,9 @@ Name: "{commonappdata}\NzbDrone\bin"; Type: filesandordirs
Name: "{app}"; Type: filesandordirs
[Run]
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Sonarr.exe"; Description: "Open Sonarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
Filename: "{app}\Sonarr.exe"; Description: "Start Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;

View File

@@ -1,7 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import Link from 'Components/Link/Link';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -22,6 +23,7 @@ function HistoryDetails(props) {
const {
indexer,
releaseGroup,
preferredWordScore,
nzbInfoUrl,
downloadClient,
downloadId,
@@ -40,24 +42,35 @@ function HistoryDetails(props) {
/>
{
!!indexer &&
indexer ?
<DescriptionListItem
title="Indexer"
data={indexer}
/>
/> :
null
}
{
!!releaseGroup &&
releaseGroup ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Release Group"
data={releaseGroup}
/>
/> :
null
}
{
!!nzbInfoUrl &&
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
{
nzbInfoUrl ?
<span>
<DescriptionListItemTitle>
Info URL
@@ -66,39 +79,44 @@ function HistoryDetails(props) {
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
</span> :
null
}
{
!!downloadClient &&
downloadClient ?
<DescriptionListItem
title="Download Client"
data={downloadClient}
/>
/> :
null
}
{
!!downloadId &&
downloadId ?
<DescriptionListItem
title="Grab ID"
data={downloadId}
/>
/> :
null
}
{
!!indexer &&
age || ageHours || ageMinutes ?
<DescriptionListItem
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/>
/> :
null
}
{
!!publishedDate &&
publishedDate ?
<DescriptionListItem
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
/> :
null
}
</DescriptionList>
);
@@ -118,11 +136,12 @@ function HistoryDetails(props) {
/>
{
!!message &&
message ?
<DescriptionListItem
title="Message"
data={message}
/>
/> :
null
}
</DescriptionList>
);
@@ -130,6 +149,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFolderImported') {
const {
preferredWordScore,
droppedPath,
importedPath
} = data;
@@ -143,21 +163,32 @@ function HistoryDetails(props) {
/>
{
!!droppedPath &&
droppedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Source"
data={droppedPath}
/>
/> :
null
}
{
!!importedPath &&
importedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Imported To"
data={importedPath}
/>
/> :
null
}
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
</DescriptionList>
);
@@ -165,7 +196,8 @@ function HistoryDetails(props) {
if (eventType === 'episodeFileDeleted') {
const {
reason
reason,
preferredWordScore
} = data;
let reasonMessage = '';
@@ -195,6 +227,15 @@ function HistoryDetails(props) {
title="Reason"
data={reasonMessage}
/>
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
</DescriptionList>
);
}
@@ -246,11 +287,12 @@ function HistoryDetails(props) {
/>
{
!!message &&
message ?
<DescriptionListItem
title="Message"
data={message}
/>
/> :
null
}
</DescriptionList>
);

View File

@@ -10,6 +10,12 @@
width: 80px;
}
.preferredWordScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
.releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
@@ -194,6 +195,17 @@ class HistoryRow extends Component {
);
}
if (name === 'preferredWordScore') {
return (
<TableRowCell
key={name}
className={styles.preferredWordScore}
>
{formatPreferredWordScore(data.preferredWordScore)}
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell

View File

@@ -44,12 +44,8 @@ class Queue extends Component {
};
}
shouldComponentUpdate(nextProps) {
if (!this._shouldBlockRefresh) {
return true;
}
if (hasDifferentItems(this.props.items, nextProps.items)) {
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
@@ -115,19 +111,20 @@ class Queue extends Component {
}
onRemoveSelectedPress = () => {
this._shouldBlockRefresh = true;
this.setState({ isConfirmRemoveModalOpen: true });
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
}
onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
this._shouldBlockRefresh = false;
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false });
}
//

View File

@@ -3,3 +3,7 @@
width: 30px;
}
.noMessages {
margin-bottom: 10px;
}

View File

@@ -12,7 +12,10 @@ function getDetailedPopoverBody(statusMessages) {
{
statusMessages.map(({ title, messages }) => {
return (
<div key={title}>
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{

View File

@@ -30,9 +30,7 @@ class AddNewSeriesModalContent extends Component {
this.state = {
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
props.seriesType.value :
props.initialSeriesType,
searchForMissingEpisodes: false,
searchForCutoffUnmetEpisodes: false
props.initialSeriesType
};
}
@@ -45,14 +43,6 @@ class AddNewSeriesModalContent extends Component {
//
// Listeners
onSearchForMissingEpisodesChange = ({ value }) => {
this.setState({ searchForMissingEpisodes: value });
}
onSearchForCutoffUnmetEpisodesChange = ({ value }) => {
this.setState({ searchForCutoffUnmetEpisodes: value });
}
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
}
@@ -63,14 +53,10 @@ class AddNewSeriesModalContent extends Component {
onAddSeriesPress = () => {
const {
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
seriesType
} = this.state;
this.props.onAddSeriesPress(
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
seriesType
);
}
@@ -91,6 +77,8 @@ class AddNewSeriesModalContent extends Component {
languageProfileId,
seriesType,
seasonFolder,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
folder,
tags,
showLanguageProfile,
@@ -101,11 +89,6 @@ class AddNewSeriesModalContent extends Component {
...otherProps
} = this.props;
const {
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -271,8 +254,8 @@ class AddNewSeriesModalContent extends Component {
containerClassName={styles.searchInputContainer}
className={styles.searchInput}
name="searchForMissingEpisodes"
value={searchForMissingEpisodes}
onChange={this.onSearchForMissingEpisodesChange}
onChange={onInputChange}
{...searchForMissingEpisodes}
/>
</label>
@@ -285,8 +268,8 @@ class AddNewSeriesModalContent extends Component {
containerClassName={styles.searchInputContainer}
className={styles.searchInput}
name="searchForCutoffUnmetEpisodes"
value={searchForCutoffUnmetEpisodes}
onChange={this.onSearchForCutoffUnmetEpisodesChange}
onChange={onInputChange}
{...searchForCutoffUnmetEpisodes}
/>
</label>
</div>
@@ -319,6 +302,8 @@ AddNewSeriesModalContent.propTypes = {
languageProfileId: PropTypes.object,
seriesType: PropTypes.object.isRequired,
seasonFolder: PropTypes.object.isRequired,
searchForMissingEpisodes: PropTypes.object.isRequired,
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,

View File

@@ -55,7 +55,7 @@ class AddNewSeriesModalContentConnector extends Component {
this.props.setAddSeriesDefault({ [name]: value });
}
onAddSeriesPress = (searchForMissingEpisodes, searchForCutoffUnmetEpisodes, seriesType) => {
onAddSeriesPress = (seriesType) => {
const {
tvdbId,
rootFolderPath,
@@ -63,6 +63,8 @@ class AddNewSeriesModalContentConnector extends Component {
qualityProfileId,
languageProfileId,
seasonFolder,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
tags
} = this.props;
@@ -74,9 +76,9 @@ class AddNewSeriesModalContentConnector extends Component {
languageProfileId: languageProfileId.value,
seriesType,
seasonFolder: seasonFolder.value,
tags: tags.value,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes
searchForMissingEpisodes: searchForMissingEpisodes.value,
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
tags: tags.value
});
}
@@ -102,6 +104,8 @@ AddNewSeriesModalContentConnector.propTypes = {
languageProfileId: PropTypes.object,
seriesType: PropTypes.object.isRequired,
seasonFolder: PropTypes.object.isRequired,
searchForMissingEpisodes: PropTypes.object.isRequired,
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired,
setAddSeriesDefault: PropTypes.func.isRequired,

View File

@@ -85,3 +85,21 @@
display: inline-block;
margin: 5px -5px 5px 0;
}
.mobileCloseButtonContainer {
display: flex;
justify-content: flex-end;
height: 40px;
border-bottom: 1px solid $borderColor;
}
.mobileCloseButton {
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
&:hover {
color: $modalCloseButtonHoverColor;
}
}

View File

@@ -518,6 +518,18 @@ class EnhancedSelectInput extends Component {
scrollDirection={scrollDirections.NONE}
>
<Scroller className={styles.optionsModalScroller}>
<div className={styles.mobileCloseButtonContainer}>
<Link
className={styles.mobileCloseButton}
onPress={this.onOptionsModalClose}
>
<Icon
name={icons.CLOSE}
size={18}
/>
</Link>
</div>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;

View File

@@ -54,4 +54,8 @@
&:last-child {
border: none;
}
&:hover {
background-color: unset;
}
}

View File

@@ -12,7 +12,9 @@ class EnhancedSelectInputOption extends Component {
//
// Listeners
onPress = () => {
onPress = (e) => {
e.preventDefault();
const {
id,
onSelect

View File

@@ -10,13 +10,16 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
(state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange,
(rootFolders, includeNoChange) => {
(rootFolders, value, includeMissingValue, includeNoChange) => {
const values = rootFolders.items.map((rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace
freeSpace: rootFolder.freeSpace,
isMissing: false
};
});
@@ -24,7 +27,8 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: 'No Change',
isDisabled: true
isDisabled: true,
isMissing: false
});
}
@@ -37,6 +41,15 @@ function createMapStateToProps() {
});
}
if (includeMissingValue && !values.find((v) => v.key === value)) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true
});
}
values.push({
key: ADD_NEW_KEY,
value: 'Add a new path'

View File

@@ -27,3 +27,9 @@
color: $darkGray;
font-size: $smallFontSize;
}
.isMissing {
margin-left: 15px;
color: $dangerColor;
font-size: $smallFontSize;
}

View File

@@ -10,6 +10,7 @@ function RootFolderSelectInputOption(props) {
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
@@ -43,11 +44,20 @@ function RootFolderSelectInputOption(props) {
</div>
{
freeSpace != null &&
freeSpace == null ?
null :
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
</div>
}
{
isMissing ?
<div className={styles.isMissing}>
Missing
</div> :
null
}
</div>
</EnhancedSelectInputOption>
);
@@ -57,6 +67,7 @@ RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
isMissing: PropTypes.boolean,
seriesFolder: PropTypes.string,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool

View File

@@ -1,5 +1,5 @@
.loadingMessage {
margin: 50px 10px 0;
margin: 10px 10px 0;
text-align: center;
font-weight: 300;
font-size: 36px;

View File

@@ -1,3 +1,12 @@
.page {
composes: page from '~./Page.css';
}
.logoFull {
margin-top: 50px;
margin-right: auto;
margin-left: auto;
width: 48px;
height: 48px;
opacity: 0.65;
}

View File

@@ -3,9 +3,15 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import LoadingMessage from 'Components/Loading/LoadingMessage';
import styles from './LoadingPage.css';
const sonarrLogo = 'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjIxNi45IiB2aWV3Qm94PSIwIDAgMjE2LjcgMjE2LjkiIHdpZHRoPSIyMTYuNyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMTYuNyAxMDguNDVjMCAyOS44MzMtMTAuNTMzIDU1LjQtMzEuNiA3Ni43LS43LjgzMy0xLjQ4MyAxLjYtMi4zNSAyLjMtMy40NjYgMy40LTcuMTMzIDYuNDg0LTExIDkuMjUtMTguMjY3IDEzLjQ2Ny0zOS4zNjcgMjAuMi02My4zIDIwLjItMjMuOTY3IDAtNDUuMDMzLTYuNzMzLTYzLjItMjAuMi00LjgtMy40LTkuMy03LjI1LTEzLjUtMTEuNTUtMTYuMzY3LTE2LjI2Ni0yNi40MTctMzUuMTY3LTMwLjE1LTU2LjctLjczMy00LjItMS4yMTctOC40NjctMS40NS0xMi44LS4xLTIuNC0uMTUtNC44LS4xNS03LjIgMC0yLjUzMy4wNS00Ljk1LjE1LTcuMjUgMC0uMjMzLjA2Ni0uNDY3LjItLjcgMS41NjctMjYuNiAxMi4wMzMtNDkuNTgzIDMxLjQtNjguOTVDNTMuMDUgMTAuNTE3IDc4LjYxNyAwIDEwOC40NSAwYzI5LjkzMyAwIDU1LjQ4NCAxMC41MTcgNzYuNjUgMzEuNTUgMjEuMDY3IDIxLjQzMyAzMS42IDQ3LjA2NyAzMS42IDc2Ljl6IiBmaWxsPSIjRUVFIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xOTQuNjUgNDIuNWwtMjIuNCAyMi40QzE1OS4xNTIgNzcuOTk4IDE1OCA4OS40IDE1OCAxMDkuNWMwIDE3LjkzNCAyLjg1MiAzNC4zNTIgMTYuMiA0Ny43IDkuNzQ2IDkuNzQ2IDE5IDE4Ljk1IDE5IDE4Ljk1LTIuNSAzLjA2Ny01LjIgNi4wNjctOC4xIDktLjcuODMzLTEuNDgzIDEuNi0yLjM1IDIuMy0yLjUzMyAyLjUtNS4xNjcgNC44MTctNy45IDYuOTVsLTE3LjU1LTE3LjU1Yy0xNS41OTgtMTUuNi0yNy45OTYtMTcuMS00OC42LTE3LjEtMTkuNzcgMC0zMy4yMjMgMS44MjItNDcuNyAxNi4zLTguNjQ3IDguNjQ3LTE4LjU1IDE4LjYtMTguNTUgMTguNi0zLjc2Ny0yLjg2Ny03LjMzMy02LjAzNC0xMC43LTkuNS0yLjgtMi44LTUuNDE3LTUuNjY3LTcuODUtOC42IDAgMCA5Ljc5OC05Ljg0OCAxOS4xNS0xOS4yIDEzLjg1Mi0xMy44NTMgMTYuMS0yOS45MTYgMTYuMS00Ny44NSAwLTE3LjUtMi44NzQtMzMuODIzLTE1LjYtNDYuNTUtOC44MzUtOC44MzYtMjEuMDUtMjEtMjEuMDUtMjEgMi44MzMtMy42IDUuOTE3LTcuMDY3IDkuMjUtMTAuNCAyLjkzNC0yLjg2NyA1LjkzNC01LjU1IDktOC4wNUw2MS4xIDQzLjg1Qzc0LjEwMiA1Ni44NTIgOTAuNzY3IDYwLjIgMTA4LjcgNjAuMmMxOC40NjcgMCAzNS4wNzctMy41NzcgNDguNi0xNy4xIDguMzItOC4zMiAxOS4zLTE5LjI1IDE5LjMtMTkuMjUgMi45IDIuMzY3IDUuNzMzIDQuOTMzIDguNSA3LjcgMy40NjcgMy41MzMgNi42NSA3LjE4MyA5LjU1IDEwLjk1eiIgZmlsbD0iIzNBM0Y1MSIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgPGcgY2xpcC1ydWxlPSJldmVub2RkIj4KICAgIDxwYXRoIGQ9Ik03OC43IDExNGMtLjItMS4xNjctLjMzMi0yLjM1LS40LTMuNTUtLjAzMi0uNjY3LS4wNS0xLjMzMy0uMDUtMiAwLS43LjAxOC0xLjM2Ny4wNS0yIDAtLjA2Ny4wMTgtLjEzMy4wNS0uMi40MzUtNy4zNjcgMy4zMzQtMTMuNzMzIDguNy0xOS4xIDUuOS01LjgzMyAxMi45ODQtOC43NSAyMS4yNS04Ljc1IDguMyAwIDE1LjM4NCAyLjkxNyAyMS4yNSA4Ljc1IDUuODM0IDUuOTM0IDguNzUgMTMuMDMzIDguNzUgMjEuMyAwIDguMjY3LTIuOTE2IDE1LjM1LTguNzUgMjEuMjUtLjIuMjMzLS40MTYuNDUtLjY1LjY1LS45NjYuOTMzLTEuOTgyIDEuNzgzLTMuMDUgMi41NS01LjA2NSAzLjczMy0xMC45MTYgNS42LTE3LjU1IDUuNnMtMTIuNDY2LTEuODY2LTE3LjUtNS42Yy0xLjMzMi0uOTM0LTIuNTgyLTItMy43NS0zLjItNC41MzItNC41LTcuMzE2LTkuNzM0LTguMzUtMTUuN3oiIGZpbGw9IiMwQ0YiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZD0iTTE1Ny44IDU5Ljc1bC0xNSAxNC42NU0zMC43ODUgMzIuNTI2TDcxLjY1IDczLjI1bTg0LjYgODQuMjVsMjcuODA4IDI4Ljc4bTEuODU1LTE1My44OTRMMTU3LjggNTkuNzVtLTEyNS40NSAxMjZsMjcuMzUtMjcuNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMENGIiBzdHJva2UtbWl0ZXJsaW1pdD0iMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICA8cGF0aCBkPSJNMTU3LjggNTkuNzVsLTE2Ljk1IDE3LjJNNTguOTcgNjAuNjA0bDE3LjIgMTcuMTVNNTkuNjIzIDE1OC40M2wxNi43NS0xNy40bTYxLjkyOC0xLjM5NmwxOC4wMjggMTcuOTQ1IiBmaWxsPSJub25lIiBzdHJva2U9IiMwQ0YiIHN0cm9rZS1taXRlcmxpbWl0PSIxIiBzdHJva2Utd2lkdGg9IjciLz4KICA8L2c+Cjwvc3ZnPg==';
function LoadingPage() {
return (
<div className={styles.page}>
<img
className={styles.logoFull}
src={sonarrLogo}
/>
<LoadingMessage />
<LoadingIndicator />
</div>

View File

@@ -191,7 +191,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumnDragSource
key={name}
name={name}
label={label || columnLabel}
label={columnLabel || label}
isVisible={isVisible}
isModifiable={true}
index={index}
@@ -209,7 +209,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumn
key={name}
name={name}
label={label || columnLabel}
label={columnLabel || label}
isVisible={isVisible}
index={index}
isModifiable={false}

View File

@@ -14,6 +14,7 @@
&.inverse {
background-color: $themeDarkColor;
box-shadow: 0 5px 10px $popoverShadowInverseColor;
color: $white;
}
}

View File

@@ -6,6 +6,24 @@ import EpisodeDetailsModalContentConnector from './EpisodeDetailsModalContentCon
class EpisodeDetailsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
closeOnBackgroundClick: false
};
}
//
// Listeners
onTabChange = (isSearch) => {
this.setState({ closeOnBackgroundClick: !isSearch });
}
//
// Render
@@ -20,10 +38,12 @@ class EpisodeDetailsModal extends Component {
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={this.state.closeOnBackgroundClick}
onModalClose={onModalClose}
>
<EpisodeDetailsModalContentConnector
{...otherProps}
onTabChange={this.onTabChange}
onModalClose={onModalClose}
/>
</Modal>

View File

@@ -37,7 +37,9 @@ class EpisodeDetailsModalContent extends Component {
// Listeners
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTab: tabs[index] });
const selectedTab = tabs[index];
this.props.onTabChange(selectedTab === 'search');
this.setState({ selectedTab });
}
//
@@ -206,6 +208,7 @@ EpisodeDetailsModalContent.propTypes = {
selectedTab: PropTypes.string.isRequired,
startInteractiveSearch: PropTypes.bool.isRequired,
onMonitorEpisodePress: PropTypes.func.isRequired,
onTabChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -300,7 +300,7 @@ class InteractiveImportModalContent extends Component {
isPopulated && !!items.length && !isFetching && !isFetching &&
<Table
columns={columns}
horizontalScroll={false}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -193,8 +194,7 @@ class InteractiveSearchRow extends Component {
</TableRowCell>
<TableRowCell className={styles.preferredWordScore}>
{preferredWordScore > 0 && `+${preferredWordScore}`}
{preferredWordScore < 0 && preferredWordScore}
{formatPreferredWordScore(preferredWordScore)}
</TableRowCell>
<TableRowCell className={styles.rejected}>

View File

@@ -26,7 +26,7 @@ function SeriesAlternateTitles({ alternateTitles }) {
}
SeriesAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default SeriesAlternateTitles;

View File

@@ -119,6 +119,13 @@
margin: 5px 10px 5px 0;
}
.fileCountMessage {
padding: 5px;
white-space: nowrap;
font-weight: 300;
font-size: 15px;
}
.path,
.sizeOnDisk,
.qualityProfileName,

View File

@@ -432,22 +432,32 @@ class SeriesDetails extends Component {
</span>
</Label>
<Label
className={styles.detailsLabel}
title={episodeFilesCountMessage}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk || 0)
}
</span>
</Label>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk || 0)
}
</span>
</Label>
}
tooltip={
<span>
{episodeFilesCountMessage}
</span>
}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
<Label
className={styles.detailsLabel}
@@ -694,7 +704,7 @@ SeriesDetails.propTypes = {
overview: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
seasons: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,

View File

@@ -131,6 +131,7 @@ function EditImportListModalContent(props) {
name="rootFolderPath"
helpText={'Root Folder list items will be added to'}
{...rootFolderPath}
includeMissingValue={true}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -23,8 +23,8 @@ const separatorOptions = [
const caseOptions = [
{ key: 'title', value: 'Default Case' },
{ key: 'lower', value: 'Lower Case' },
{ key: 'upper', value: 'Upper Case' }
{ key: 'lower', value: 'Lowercase' },
{ key: 'upper', value: 'Uppercase' }
];
const fileNameTokens = [

View File

@@ -9,3 +9,12 @@
margin-bottom: 30px;
}
.triggers {
margin-top: 3px;
}
.triggerEvents {
margin-top: 10px;
user-select: none;
}

View File

@@ -13,6 +13,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import styles from './EditNotificationModalContent.css';
@@ -102,131 +103,110 @@ function EditNotificationModalContent(props) {
</FormGroup>
<FormGroup>
<FormLabel>On Grab</FormLabel>
<FormLabel>Triggers</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onGrab"
helpText="Be notified when episodes are available for download and has been sent to a download client"
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>On Import</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onDownload"
helpText="Be notified when episodes are successfully imported"
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
/>
</FormGroup>
{
onDownload.value &&
<FormGroup>
<FormLabel>On Upgrade</FormLabel>
<div className={styles.triggers}>
<FormInputHelpText
text="Select which events should trigger this conection"
link="https://wiki.servarr.com/Sonarr_Settings#Connections"
/>
<div className={styles.triggerEvents}>
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText="Be notified when episodes are upgraded to a better quality"
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
name="onGrab"
helpText="On Grab"
isDisabled={!supportsOnGrab.value}
{...onGrab}
onChange={onInputChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>On Rename</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onRename"
helpText="Be notified when episodes are renamed"
isDisabled={!supportsOnRename.value}
{...onRename}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>On Series Delete</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onSeriesDelete"
helpText="Be notified when series are deleted"
isDisabled={!supportsOnSeriesDelete.value}
{...onSeriesDelete}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>On Episode File Delete</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDelete"
helpText="Be notified when episode files are deleted"
isDisabled={!supportsOnEpisodeFileDelete.value}
{...onEpisodeFileDelete}
onChange={onInputChange}
/>
</FormGroup>
{
onEpisodeFileDelete.value ?
<FormGroup>
<FormLabel>On Episode File Delete For Upgrade</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText="Be notified when episode files are deleted for upgrades"
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
{...onEpisodeFileDeleteForUpgrade}
name="onDownload"
helpText="On Import"
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>On Health Issue</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="onHealthIssue"
helpText="Be notified on health check failures"
isDisabled={!supportsOnHealthIssue.value}
{...onHealthIssue}
onChange={onInputChange}
/>
</FormGroup>
{
onHealthIssue.value &&
<FormGroup>
<FormLabel>Include Health Warnings</FormLabel>
{
onDownload.value ?
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText="On Upgrade"
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
onChange={onInputChange}
/> :
null
}
<FormInputGroup
type={inputTypes.CHECK}
name="includeHealthWarnings"
helpText="Be notified on health warnings in addition to errors"
name="onRename"
helpText="On Rename"
isDisabled={!supportsOnRename.value}
{...onRename}
onChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.CHECK}
name="onSeriesDelete"
helpText="On Series Delete"
isDisabled={!supportsOnSeriesDelete.value}
{...onSeriesDelete}
onChange={onInputChange}
/>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDelete"
helpText="On Episode File Delete"
isDisabled={!supportsOnEpisodeFileDelete.value}
{...onEpisodeFileDelete}
onChange={onInputChange}
/>
{
onEpisodeFileDelete.value ?
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText="On Episode File Delete For Upgrade"
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
{...onEpisodeFileDeleteForUpgrade}
onChange={onInputChange}
/> :
null
}
<FormInputGroup
type={inputTypes.CHECK}
name="onHealthIssue"
helpText="On Health Issue"
isDisabled={!supportsOnHealthIssue.value}
{...includeHealthWarnings}
{...onHealthIssue}
onChange={onInputChange}
/>
</FormGroup>
}
{
onHealthIssue.value ?
<FormInputGroup
type={inputTypes.CHECK}
name="includeHealthWarnings"
helpText="Include Health Warnings"
isDisabled={!supportsOnHealthIssue.value}
{...includeHealthWarnings}
onChange={onInputChange}
/> :
null
}
</div>
</div>
</FormGroup>
<FormGroup>
<FormLabel>Tags</FormLabel>

View File

@@ -39,6 +39,7 @@ function EditDelayProfileModalContent(props) {
enableTorrent,
usenetDelay,
torrentDelay,
bypassIfHighestQuality,
tags
} = item;
@@ -107,6 +108,20 @@ function EditDelayProfileModalContent(props) {
</FormGroup>
}
{
<FormGroup>
<FormLabel>Bypass if Highest Quality</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="bypassIfHighestQuality"
{...bypassIfHighestQuality}
helpText="Bypass delay when release has the highest enabled quality in the quality profile"
onChange={onInputChange}
/>
</FormGroup>
}
{
id === 1 ?
<Alert>

View File

@@ -29,6 +29,7 @@ function EditReleaseProfileModalContent(props) {
const {
id,
name,
enabled,
required,
ignored,
@@ -46,6 +47,20 @@ function EditReleaseProfileModalContent(props) {
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>Name</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
placeholder="Optional name"
canEdit={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Enable Profile</FormLabel>

View File

@@ -9,3 +9,11 @@
flex-wrap: wrap;
margin-top: 5px;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}

View File

@@ -56,6 +56,7 @@ class ReleaseProfile extends Component {
render() {
const {
id,
name,
enabled,
required,
ignored,
@@ -79,6 +80,14 @@ class ReleaseProfile extends Component {
overlayContent={true}
onPress={this.onEditReleaseProfilePress}
>
{
name ?
<div className={styles.name}>
{name}
</div> :
null
}
<div>
{
split(required).map((item) => {
@@ -184,6 +193,7 @@ class ReleaseProfile extends Component {
ReleaseProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string,
enabled: PropTypes.bool.isRequired,
required: PropTypes.string.isRequired,
ignored: PropTypes.string.isRequired,

View File

@@ -37,6 +37,8 @@ export const defaultState = {
languageProfileId: 0,
seriesType: seriesTypes.STANDARD,
seasonFolder: true,
searchForMissingEpisodes: false,
searchForCutoffUnmetEpisodes: false,
tags: []
}
};

View File

@@ -1,7 +1,9 @@
import _ from 'lodash';
import { createAction } from 'redux-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import { createThunk, handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
function getDimensions(width, height) {
@@ -22,6 +24,8 @@ function getDimensions(width, height) {
export const section = 'app';
const messagesSection = 'app.messages';
let abortPingServer = null;
let pingTimeout = null;
//
// State
@@ -50,6 +54,8 @@ export const SET_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue';
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
export const PING_SERVER = 'app/pingServer';
//
// Action Creators
@@ -59,6 +65,70 @@ export const setIsSidebarVisible = createAction(SET_IS_SIDEBAR_VISIBLE);
export const setAppValue = createAction(SET_APP_VALUE);
export const showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE);
export const pingServer = createThunk(PING_SERVER);
//
// Helpers
function pingServerAfterTimeout(getState, dispatch) {
if (abortPingServer) {
abortPingServer();
abortPingServer = null;
}
if (pingTimeout) {
clearTimeout(pingTimeout);
pingTimeout = null;
}
pingTimeout = setTimeout(() => {
if (!getState().isRestarting && getState().isConnected) {
return;
}
const ajaxOptions = {
url: '/system/status',
method: 'GET',
contentType: 'application/json'
};
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
abortPingServer = abortRequest;
request.done(() => {
abortPingServer = null;
pingTimeout = null;
dispatch(setAppValue({
isRestarting: false
}));
});
request.fail((xhr) => {
abortPingServer = null;
pingTimeout = null;
// Unauthorized, but back online
if (xhr.status === 401) {
dispatch(setAppValue({
isRestarting: false
}));
} else {
pingServerAfterTimeout(getState, dispatch);
}
});
}, 5000);
}
//
// Action Handlers
export const actionHandlers = handleThunks({
[PING_SERVER]: function(getState, payload, dispatch) {
pingServerAfterTimeout(getState, dispatch);
}
});
//
// Reducers
@@ -135,4 +205,3 @@ export const reducers = createHandleActions({
}
}, defaultState, section);

View File

@@ -86,11 +86,8 @@ export const actionHandlers = handleThunks({
} = payload;
const promise = createAjaxRequest({
url: '/history/failed',
method: 'POST',
data: {
id: historyId
}
url: `/history/failed/${historyId}`,
method: 'POST'
}).request;
promise.done(() => {

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { createAction } from 'redux-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
import { filterTypes, sortDirections } from 'Helpers/Props';
import { filterTypes, icons, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import { createThunk, handleThunks } from 'Store/thunks';
import createClearReducer from './Creators/Reducers/createClearReducer';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
@@ -80,6 +82,15 @@ export const defaultState = {
label: 'Release Group',
isVisible: false
},
{
name: 'preferredWordScore',
columnLabel: 'Preferred Word Score',
label: React.createElement(Icon, {
name: icons.SCORE,
title: 'Preferred word score'
}),
isVisible: false
},
{
name: 'details',
columnLabel: 'Details',
@@ -233,11 +244,9 @@ export const actionHandlers = handleThunks({
}));
const promise = createAjaxRequest({
url: '/history/failed',
url: `/history/failed/${id}`,
method: 'POST',
data: {
id
}
dataType: 'json'
}).request;
promise.done(() => {

View File

@@ -63,6 +63,8 @@ export const defaultState = {
};
export const persistState = [
'interactiveImport.sortKey',
'interactiveImport.sortDirection',
'interactiveImport.recentFolders',
'interactiveImport.importMode'
];

View File

@@ -8,7 +8,7 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createHandleActions from './Creators/createHandleActions';
import { set, updateItem } from './baseActions';
import { filters, filterPredicates, filterBuilderProps } from './seriesActions';
import { filters, filterPredicates, filterBuilderProps, sortPredicates } from './seriesActions';
//
// Variables
@@ -27,6 +27,7 @@ export const defaultState = {
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'sortTitle',
secondarySortDirection: sortDirections.ASCENDING,
sortPredicates,
selectedFilterKey: 'all',
filters,
filterPredicates,
@@ -97,7 +98,8 @@ export const persistState = [
'seriesEditor.sortKey',
'seriesEditor.sortDirection',
'seriesEditor.selectedFilterKey',
'seriesEditor.customFilters'
'seriesEditor.customFilters',
'seriesEditor.columns'
];
//

View File

@@ -10,6 +10,7 @@ import createFetchHandler from './Creators/createFetchHandler';
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
import createHandleActions from './Creators/createHandleActions';
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
import { pingServer } from './appActions';
import { set } from './baseActions';
//
@@ -351,6 +352,7 @@ export const actionHandlers = handleThunks({
promise.done(() => {
dispatch(setAppValue({ isRestarting: true }));
dispatch(pingServer());
});
},

View File

@@ -29,27 +29,32 @@ function mergeColumns(path, initialState, persistedState, computedState) {
const columns = [];
initialColumns.forEach((initialColumn) => {
const persistedColumnIndex = _.findIndex(persistedColumns, { name: initialColumn.name });
const column = Object.assign({}, initialColumn);
const persistedColumn = persistedColumnIndex > -1 ? persistedColumns[persistedColumnIndex] : undefined;
// Add persisted columns in the same order they're currently in
// as long as they haven't been removed.
if (persistedColumn) {
column.isVisible = persistedColumn.isVisible;
persistedColumns.forEach((persistedColumn) => {
const column = initialColumns.find((i) => i.name === persistedColumn.name);
if (column) {
columns.push({
...column,
isVisible: persistedColumn.isVisible
});
}
// If there is a persisted column, it's index doesn't exceed the column list
// and it's modifiable, insert it in the proper position.
if (persistedColumn && columns.length - 1 > persistedColumnIndex && persistedColumn.isModifiable !== false) {
columns.splice(persistedColumnIndex, 0, column);
} else {
columns.push(column);
}
// Set the columns in the persisted state
_.set(computedState, path, columns);
});
// Add any columns added to the app in the initial position.
initialColumns.forEach((initialColumn, index) => {
const persistedColumnIndex = persistedColumns.findIndex((i) => i.name === initialColumn.name);
const column = Object.assign({}, initialColumn);
if (persistedColumnIndex === -1) {
columns.splice(index, 0, column);
}
});
// Set the columns in the persisted state
_.set(computedState, path, columns);
}
function slicer(paths_) {

View File

@@ -36,7 +36,7 @@ class MoreInfo extends Component {
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://discord.gg/M6BvZn5">discord.gg/M6BvZn5</Link>
<Link to="https://discord.gg/73QUuf3bgA">discord.gg/73QUuf3bgA</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>IRC</DescriptionListItemTitle>

View File

@@ -0,0 +1,16 @@
function formatPreferredWordScore(input) {
const score = Number(input);
if (score > 0) {
return `+${score}`;
}
if (score < 0) {
return score;
}
return '';
}
export default formatPreferredWordScore;

View File

@@ -28,6 +28,15 @@ function addApiKey(ajaxOptions) {
ajaxOptions.headers['X-Api-Key'] = window.Sonarr.apiKey;
}
function addContentType(ajaxOptions) {
if (
ajaxOptions.contentType == null &&
ajaxOptions.dataType === 'json' &&
(ajaxOptions.method === 'PUT' || ajaxOptions.method === 'POST')) {
ajaxOptions.contentType = 'application/json';
}
}
export default function createAjaxRequest(originalAjaxOptions) {
const requestXHR = new window.XMLHttpRequest();
let aborted = false;
@@ -46,6 +55,7 @@ export default function createAjaxRequest(originalAjaxOptions) {
moveBodyToQuery(ajaxOptions);
addRootUrl(ajaxOptions);
addApiKey(ajaxOptions);
addContentType(ajaxOptions);
}
const request = $.ajax({

View File

@@ -30,7 +30,7 @@
sizes="16x16"
href="/Content/Images/Icons/favicon-16x16.png"
/>
<link rel="manifest" href="/Content/Images/Icons/manifest.json" />
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
<link
rel="mask-icon"
href="/Content/Images/Icons/safari-pinned-tab.svg"

View File

@@ -30,7 +30,7 @@
sizes="16x16"
href="/Content/Images/Icons/favicon-16x16.png"
/>
<link rel="manifest" href="/Content/Images/Icons/manifest.json" />
<link rel="manifest" href="/Content/Images/Icons/manifest.json" crossorigin="use-credentials" />
<link
rel="mask-icon"
href="/Content/Images/Icons/safari-pinned-tab.svg"

View File

@@ -13,6 +13,7 @@ namespace NzbDrone.Api.Profiles.Delay
public DownloadProtocol PreferredProtocol { get; set; }
public int UsenetDelay { get; set; }
public int TorrentDelay { get; set; }
public bool BypassIfHighestQuality { get; set; }
public int Order { get; set; }
public HashSet<int> Tags { get; set; }
}
@@ -32,6 +33,7 @@ namespace NzbDrone.Api.Profiles.Delay
PreferredProtocol = model.PreferredProtocol,
UsenetDelay = model.UsenetDelay,
TorrentDelay = model.TorrentDelay,
BypassIfHighestQuality = model.BypassIfHighestQuality,
Order = model.Order,
Tags = new HashSet<int>(model.Tags)
};
@@ -50,6 +52,7 @@ namespace NzbDrone.Api.Profiles.Delay
PreferredProtocol = resource.PreferredProtocol,
UsenetDelay = resource.UsenetDelay,
TorrentDelay = resource.TorrentDelay,
BypassIfHighestQuality = resource.BypassIfHighestQuality,
Order = resource.Order,
Tags = new HashSet<int>(resource.Tags)
};

View File

@@ -179,6 +179,38 @@ namespace NzbDrone.Common.Test.Http
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_not_throw_on_suppressed_status_codes()
{
var request = new HttpRequest($"http://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.NotFound };
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"http://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_log_unsuccessful_status_codes()
{
var request = new HttpRequest($"http://{_httpBinHost}/status/{HttpStatusCode.NotFound}");
request.LogHttpError = false;
var exception = Assert.Throws<HttpException>(() => Subject.Get<HttpBinResource>(request));
ExceptionVerification.ExpectedWarns(0);
}
[Test]
public void should_not_follow_redirects_when_not_in_production()
{

View File

@@ -48,6 +48,19 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase(@"""DownloadURL"":""https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=mySecret&torrent_pass=mySecret""")]
// Plex
[TestCase(@" http://localhost:32400/library/metadata/12345/refresh?X-Plex-Client-Identifier=1234530f-422f-4aac-b6b3-01233210aaaa&X-Plex-Product=Sonarr&X-Plex-Platform=Windows&X-Plex-Platform-Version=7&X-Plex-Device-Name=Sonarr&X-Plex-Version=3.0.3.833&X-Plex-Token=mySecret")]
// Internal
[TestCase(@"OutputPath=/home/mySecret/Downloads")]
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
public void should_clean_message(string message)
{
var cleansedMessage = CleanseLogMessage.Cleanse(message);

View File

@@ -88,5 +88,38 @@ namespace NzbDrone.Common.Test.TPLTests
(GetRateLimitStore()["me"] - _epoch).Should().BeGreaterOrEqualTo(TimeSpan.FromMilliseconds(100));
}
[Test]
public void should_extend_subkey_delay()
{
GivenExisting("me", _epoch + TimeSpan.FromMilliseconds(200));
GivenExisting("me-sub", _epoch + TimeSpan.FromMilliseconds(300));
Subject.WaitAndPulse("me", "sub", TimeSpan.FromMilliseconds(100));
(GetRateLimitStore()["me-sub"] - _epoch).Should().BeGreaterOrEqualTo(TimeSpan.FromMilliseconds(400));
}
[Test]
public void should_honor_basekey_delay()
{
GivenExisting("me", _epoch + TimeSpan.FromMilliseconds(200));
GivenExisting("me-sub", _epoch + TimeSpan.FromMilliseconds(0));
Subject.WaitAndPulse("me", "sub", TimeSpan.FromMilliseconds(100));
(GetRateLimitStore()["me-sub"] - _epoch).Should().BeGreaterOrEqualTo(TimeSpan.FromMilliseconds(200));
}
[Test]
public void should_not_extend_basekey_delay()
{
GivenExisting("me", _epoch + TimeSpan.FromMilliseconds(200));
GivenExisting("me-sub", _epoch + TimeSpan.FromMilliseconds(100));
Subject.WaitAndPulse("me", "sub", TimeSpan.FromMilliseconds(100));
(GetRateLimitStore()["me"] - _epoch).Should().BeCloseTo(TimeSpan.FromMilliseconds(200));
}
}
}

View File

@@ -85,8 +85,7 @@ namespace NzbDrone.Common.EnvironmentInfo
if (_diskProvider.FileExists(_appFolderInfo.GetDatabase())) return;
if (!_diskProvider.FileExists(oldDbFile)) return;
_diskProvider.MoveFile(oldDbFile, _appFolderInfo.GetDatabase());
CleanupSqLiteRollbackFiles();
MoveSqliteDatabase(oldDbFile, _appFolderInfo.GetDatabase());
RemovePidFile();
}
@@ -108,12 +107,9 @@ namespace NzbDrone.Common.EnvironmentInfo
// Rename the DB file
if (_diskProvider.FileExists(oldDbFile))
{
_diskProvider.MoveFile(oldDbFile, _appFolderInfo.GetDatabase());
MoveSqliteDatabase(oldDbFile, _appFolderInfo.GetDatabase());
}
// Remove SQLite rollback files
CleanupSqLiteRollbackFiles();
// Remove Old PID file
RemovePidFile();
@@ -127,7 +123,6 @@ namespace NzbDrone.Common.EnvironmentInfo
}
}
private void InitializeMonoApplicationData()
{
if (OsInfo.IsWindows) return;
@@ -158,12 +153,37 @@ namespace NzbDrone.Common.EnvironmentInfo
}
}
private void CleanupSqLiteRollbackFiles()
private void MoveSqliteDatabase(string source, string destination)
{
_diskProvider.GetFiles(_appFolderInfo.AppDataFolder, SearchOption.TopDirectoryOnly)
.Where(f => Path.GetFileName(f).StartsWith("nzbdrone.db"))
.ToList()
.ForEach(_diskProvider.DeleteFile);
_logger.Info("Moving {0}* to {1}*", source, destination);
var dbSuffixes = new[] { "", "-shm", "-wal", "-journal" };
foreach (var suffix in dbSuffixes)
{
var sourceFile = source + suffix;
var destFile = destination + suffix;
if (_diskProvider.FileExists(destFile))
{
_diskProvider.DeleteFile(destFile);
}
if (_diskProvider.FileExists(sourceFile))
{
_diskProvider.CopyFile(sourceFile, destFile);
}
}
foreach (var suffix in dbSuffixes)
{
var sourceFile = source + suffix;
if (_diskProvider.FileExists(sourceFile))
{
_diskProvider.DeleteFile(sourceFile);
}
}
}
private void RemovePidFile()

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Common.EnvironmentInfo
public const string TERMINATE = "terminateexisting";
public const string RESTART = "restart";
public const string REGISTER_URL = "registerurl";
public const string EXIT_IMMEDIATELY = "exitimmediately";
public StartupContext(params string[] args)
{
@@ -54,6 +55,7 @@ namespace NzbDrone.Common.EnvironmentInfo
public bool InstallService => Flags.Contains(INSTALL_SERVICE);
public bool UninstallService => Flags.Contains(UNINSTALL_SERVICE);
public bool RegisterUrl => Flags.Contains(REGISTER_URL);
public bool ExitImmediately => Flags.Contains(EXIT_IMMEDIATELY);
public string PreservedArguments
{

View File

@@ -85,9 +85,12 @@ namespace NzbDrone.Common.Http
_logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]);
}
if (!request.SuppressHttpError && response.HasHttpError)
if (!request.SuppressHttpError && response.HasHttpError && (request.SuppressHttpErrorStatusCodes == null || !request.SuppressHttpErrorStatusCodes.Contains(response.StatusCode)))
{
_logger.Warn("HTTP Error - {0}", response);
if (request.LogHttpError)
{
_logger.Warn("HTTP Error - {0}", response);
}
if ((int)response.StatusCode == 429)
{
@@ -111,7 +114,7 @@ namespace NzbDrone.Common.Http
if (request.RateLimit != TimeSpan.Zero)
{
_rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimit);
_rateLimitService.WaitAndPulse(request.Url.Host, request.RateLimitKey, request.RateLimit);
}
_logger.Trace(request);

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -15,6 +16,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader();
AllowAutoRedirect = true;
StoreRequestCookie = true;
LogHttpError = true;
Cookies = new Dictionary<string, string>();
@@ -35,15 +37,18 @@ namespace NzbDrone.Common.Http
public byte[] ContentData { get; set; }
public string ContentSummary { get; set; }
public bool SuppressHttpError { get; set; }
public IEnumerable<HttpStatusCode> SuppressHttpErrorStatusCodes { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
public bool LogResponseContent { get; set; }
public bool LogHttpError { get; set; }
public Dictionary<string, string> Cookies { get; private set; }
public bool StoreRequestCookie { get; set; }
public bool StoreResponseCookie { get; set; }
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }
public string RateLimitKey { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString()

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Common.Http
public Dictionary<string, string> Segments { get; private set; }
public HttpHeader Headers { get; private set; }
public bool SuppressHttpError { get; set; }
public bool LogHttpError { get; set; }
public bool UseSimplifiedUserAgent { get; set; }
public bool AllowAutoRedirect { get; set; }
public bool ConnectionKeepAlive { get; set; }
@@ -41,6 +42,7 @@ namespace NzbDrone.Common.Http
Headers = new HttpHeader();
Cookies = new Dictionary<string, string>();
FormData = new List<HttpFormData>();
LogHttpError = true;
}
public HttpRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
@@ -101,6 +103,7 @@ namespace NzbDrone.Common.Http
{
request.Method = Method;
request.SuppressHttpError = SuppressHttpError;
request.LogHttpError = LogHttpError;
request.UseSimplifiedUserAgent = UseSimplifiedUserAgent;
request.AllowAutoRedirect = AllowAutoRedirect;
request.ConnectionKeepAlive = ConnectionKeepAlive;

View File

@@ -53,7 +53,10 @@ namespace NzbDrone.Common.Http
public bool HasHttpRedirect => StatusCode == HttpStatusCode.Moved ||
StatusCode == HttpStatusCode.MovedPermanently ||
StatusCode == HttpStatusCode.Found;
StatusCode == HttpStatusCode.Found ||
StatusCode == HttpStatusCode.TemporaryRedirect ||
StatusCode == HttpStatusCode.RedirectMethod ||
StatusCode == HttpStatusCode.SeeOther;
public string[] GetCookieHeaders()
{

View File

@@ -19,9 +19,12 @@ namespace NzbDrone.Common.Instrumentation
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
new Regex(@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
// Path
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"""/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"/home/(?<secret>[^/""]+?)(/|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
// NzbGet
new Regex(@"""Name""\s*:\s*""[^""]*(username|password)""\s*,\s*""Value""\s*:\s*""(?<secret>[^""]+?)""", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -18,6 +18,12 @@ namespace NzbDrone.Common.Instrumentation
{
var exception = e.Exception;
if (exception.InnerException is ObjectDisposedException disposedException && disposedException.ObjectName == "System.Net.HttpListenerRequest")
{
// We don't care about web connections
return;
}
Console.WriteLine("Task Error: {0}", exception);
Logger.Error(exception, "Task Error");
}

View File

@@ -195,7 +195,10 @@ namespace NzbDrone.Common.Instrumentation.Sentry
if (ex != null)
{
fingerPrint.Add(ex.GetType().FullName);
fingerPrint.Add(ex.TargetSite.ToString());
if (ex.TargetSite != null)
{
fingerPrint.Add(ex.TargetSite.ToString());
}
if (ex.InnerException != null)
{
fingerPrint.Add(ex.InnerException.GetType().FullName);

View File

@@ -2,12 +2,14 @@
using System.Collections.Concurrent;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.TPL
{
public interface IRateLimitService
{
void WaitAndPulse(string key, TimeSpan interval);
void WaitAndPulse(string key, string subKey, TimeSpan interval);
}
public class RateLimitService : IRateLimitService
@@ -23,9 +25,37 @@ namespace NzbDrone.Common.TPL
public void WaitAndPulse(string key, TimeSpan interval)
{
var waitUntil = _rateLimitStore.AddOrUpdate(key,
(s) => DateTime.UtcNow + interval,
(s,i) => new DateTime(Math.Max(DateTime.UtcNow.Ticks, i.Ticks), DateTimeKind.Utc) + interval);
WaitAndPulse(key, null, interval);
}
public void WaitAndPulse(string key, string subKey, TimeSpan interval)
{
var waitUntil = DateTime.UtcNow.Add(interval);
if (subKey.IsNotNullOrWhiteSpace())
{
// Expand the base key timer, but don't extend it beyond now+interval.
var baseUntil = _rateLimitStore.AddOrUpdate(key,
(s) => waitUntil,
(s, i) => new DateTime(Math.Max(waitUntil.Ticks, i.Ticks), DateTimeKind.Utc));
if (baseUntil > waitUntil)
{
waitUntil = baseUntil;
}
// Wait for the full key
var combinedKey = key + "-" + subKey;
waitUntil = _rateLimitStore.AddOrUpdate(combinedKey,
(s) => waitUntil,
(s, i) => new DateTime(Math.Max(waitUntil.Ticks, i.Add(interval).Ticks), DateTimeKind.Utc));
}
else
{
waitUntil = _rateLimitStore.AddOrUpdate(key,
(s) => waitUntil,
(s, i) => new DateTime(Math.Max(waitUntil.Ticks, i.Add(interval).Ticks), DateTimeKind.Utc));
}
waitUntil -= interval;

View File

@@ -25,10 +25,11 @@ namespace NzbDrone.Console
public static void Main(string[] args)
{
RuntimePatcher.Initialize();
StartupContext startupArgs = null;
try
{
var startupArgs = new StartupContext(args);
startupArgs = new StartupContext(args);
try
{
NzbDroneLogger.Register(startupArgs, false, true);
@@ -45,21 +46,21 @@ namespace NzbDrone.Console
System.Console.WriteLine("");
System.Console.WriteLine("");
Logger.Fatal(ex, "EPIC FAIL!");
Exit(ExitCodes.NonRecoverableFailure);
Exit(ExitCodes.NonRecoverableFailure, startupArgs);
}
catch (SocketException ex)
{
System.Console.WriteLine("");
System.Console.WriteLine("");
Logger.Fatal(ex.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions");
Exit(ExitCodes.RecoverableFailure);
Exit(ExitCodes.RecoverableFailure, startupArgs);
}
catch (RemoteAccessException ex)
{
System.Console.WriteLine("");
System.Console.WriteLine("");
Logger.Fatal(ex, "EPIC FAIL!");
Exit(ExitCodes.Normal);
Exit(ExitCodes.Normal, startupArgs);
}
catch (Exception ex)
{
@@ -67,15 +68,15 @@ namespace NzbDrone.Console
System.Console.WriteLine("");
Logger.Fatal(ex, "EPIC FAIL!");
System.Console.WriteLine("EPIC FAIL! " + ex.ToString());
Exit(ExitCodes.UnknownFailure);
Exit(ExitCodes.UnknownFailure, startupArgs);
}
Logger.Info("Exiting main.");
Exit(ExitCodes.Normal);
Exit(ExitCodes.Normal, startupArgs);
}
private static void Exit(ExitCodes exitCode)
private static void Exit(ExitCodes exitCode, StartupContext startupArgs)
{
LogManager.Shutdown();
@@ -87,6 +88,15 @@ namespace NzbDrone.Console
if (exitCode == ExitCodes.NonRecoverableFailure)
{
if (startupArgs?.ExitImmediately == true)
{
System.Console.WriteLine("Non-recoverable failure, but set to exit immediately");
Environment.Exit((int)exitCode);
}
System.Console.WriteLine("Non-recoverable failure, waiting for user intervention...");
for (int i = 0; i < 3600; i++)
{

View File

@@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class email_multiple_addressesFixture : MigrationTest<email_multiple_addresses>
{
[Test]
public void should_convert_to_list_on_email_lists()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Notifications").Row(new
{
OnGrab = true,
OnDownload = true,
OnUpgrade = true,
OnHealthIssue = true,
IncludeHealthWarnings = true,
OnRename = true,
Name = "Mail Sonarr",
Implementation = "Email",
Tags = "[]",
Settings = new EmailSettings173
{
Server = "smtp.gmail.com",
Port = 563,
To = "dont@email.me"
}.ToJson(),
ConfigContract = "EmailSettings"
});
});
var items = db.Query<NotificationDefinition173>("SELECT * FROM Notifications");
items.Should().HaveCount(1);
items.First().Implementation.Should().Be("Email");
items.First().ConfigContract.Should().Be("EmailSettings");
items.First().Settings.To.Count().Should().Be(1);
}
}
public class NotificationDefinition173
{
public int Id { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public EmailSettings174 Settings { get; set; }
public string Name { get; set; }
public bool OnGrab { get; set; }
public bool OnDownload { get; set; }
public bool OnUpgrade { get; set; }
public bool OnRename { get; set; }
public bool OnSeriesDelete { get; set; }
public bool OnEpisodeFileDelete { get; set; }
public bool OnEpisodeFileDeleteForUpgrade { get; set; }
public bool OnHealthIssue { get; set; }
public bool SupportsOnGrab { get; set; }
public bool SupportsOnDownload { get; set; }
public bool SupportsOnUpgrade { get; set; }
public bool SupportsOnRename { get; set; }
public bool SupportsOnSeriesDelete { get; set; }
public bool SupportsOnEpisodeFileDelete { get; set; }
public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; }
public bool SupportsOnHealthIssue { get; set; }
public bool IncludeHealthWarnings { get; set; }
public List<int> Tags { get; set; }
}
public class EmailSettings173
{
public string Server { get; set; }
public int Port { get; set; }
public bool RequireEncryption { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string From { get; set; }
public string To { get; set; }
}
public class EmailSettings174
{
public string Server { get; set; }
public int Port { get; set; }
public bool RequireEncryption { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string From { get; set; }
public IEnumerable<string> To { get; set; }
public IEnumerable<string> Cc { get; set; }
public IEnumerable<string> Bcc { get; set; }
}
}

View File

@@ -25,6 +25,7 @@ namespace NzbDrone.Core.Test.Datastore.SqliteSchemaDumperTests
[TestCase(@"CREATE TABLE ""Test """"Table"" (""My""""Id"" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "Test \"Table", "My\"Id")]
[TestCase(@"CREATE TABLE [Test Table] ([My Id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT)", "Test Table", "My Id")]
[TestCase(@" CREATE TABLE `Test ``Table` ( `My`` Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ) ", "Test `Table", "My` Id")]
[TestCase(@"CREATE TABLE TestTable (MyId INTEGER NOT NULL, PRIMARY KEY(""MyId"" AUTOINCREMENT))", "TestTable", "MyId")]
public void should_parse_table_language_flavors(string sql, string tableName, string columnName)
{
var result = Subject.ReadTableSchema(sql);

View File

@@ -124,7 +124,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
}
[Test]
public void should_be_true_when_quality_and_language_is_last_allowed_in_profile()
public void should_be_false_when_quality_and_language_is_last_allowed_in_profile_and_bypass_disabled()
{
_remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray720p);
_remoteEpisode.ParsedEpisodeInfo.Language = Language.French;
@@ -132,6 +132,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_true_when_quality_and_language_is_last_allowed_in_profile_and_bypass_enabled()
{
_delayProfile.BypassIfHighestQuality = true;
_remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray720p);
_remoteEpisode.ParsedEpisodeInfo.Language = Language.French;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_true_when_release_is_older_than_delay()
{

View File

@@ -12,6 +12,7 @@ using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Download.Clients;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
@@ -71,19 +72,23 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
protected void GivenFailedDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>();
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Setup(s => s.AddTorrentFromFile(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Callback(() =>
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Hash = "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951",
Name = _title,
Size = 1000,
Progress = 1.0,
@@ -135,6 +140,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Setup(s => s.GetTorrentFiles(torrent.Hash.ToLower(), It.IsAny<QBittorrentSettings>()))
.Returns(new List<QBittorrentTorrentFile> { new QBittorrentTorrentFile { Name = torrent.Name } });
}
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.IsTorrentLoaded(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Returns<string, QBittorrentSettings>((hash, s) => torrents.Any(v => v.Hash.ToLower() == hash));
}
private void GivenTorrentFiles(string hash, List<QBittorrentTorrentFile> files)
@@ -466,7 +475,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Assert.DoesNotThrow(() => Subject.Download(remoteEpisode));
Mocker.GetMock<IQBittorrentProxy>()
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]

View File

@@ -11,6 +11,7 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Indexers;
using System.Linq;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Test.Download.TrackedDownloads
{
@@ -144,5 +145,185 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(0);
trackedDownload.RemoteEpisode.MappedSeasonNumber.Should().Be(0);
}
[Test]
public void should_unmap_tracked_download_if_episode_deleted()
{
GivenDownloadHistory();
var remoteEpisode = new RemoteEpisode
{
Series = new Series() { Id = 5 },
Episodes = new List<Episode> { new Episode { Id = 4 } },
ParsedEpisodeInfo = new ParsedEpisodeInfo()
{
SeriesTitle = "TV Series",
SeasonNumber = 1,
EpisodeNumbers = new[] { 1 }
},
MappedSeasonNumber = 0
};
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(remoteEpisode);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EpisodeHistory>());
var client = new DownloadClientDefinition()
{
Id = 1,
Protocol = DownloadProtocol.Torrent
};
var item = new DownloadClientItem()
{
Title = "TV Series - S01E01",
DownloadId = "12345",
DownloadClientInfo = new DownloadClientItemClientInfo
{
Id = 1,
Type = "Blackhole",
Name = "Blackhole Client",
Protocol = DownloadProtocol.Torrent
}
};
Subject.TrackDownload(client, item);
Subject.GetTrackedDownloads().Should().HaveCount(1);
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(default(RemoteEpisode));
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
var trackedDownloads = Subject.GetTrackedDownloads();
trackedDownloads.Should().HaveCount(1);
trackedDownloads.First().RemoteEpisode.Should().BeNull();
}
[Test]
public void should_not_throw_when_processing_deleted_episodes()
{
GivenDownloadHistory();
var remoteEpisode = new RemoteEpisode
{
Series = new Series() { Id = 5 },
Episodes = new List<Episode> { new Episode { Id = 4 } },
ParsedEpisodeInfo = new ParsedEpisodeInfo()
{
SeriesTitle = "TV Series",
SeasonNumber = 1,
EpisodeNumbers = new[] { 1 }
},
MappedSeasonNumber = 0
};
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(default(RemoteEpisode));
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EpisodeHistory>());
var client = new DownloadClientDefinition()
{
Id = 1,
Protocol = DownloadProtocol.Torrent
};
var item = new DownloadClientItem()
{
Title = "TV Series - S01E01",
DownloadId = "12345",
DownloadClientInfo = new DownloadClientItemClientInfo
{
Id = 1,
Type = "Blackhole",
Name = "Blackhole Client",
Protocol = DownloadProtocol.Torrent
}
};
Subject.TrackDownload(client, item);
Subject.GetTrackedDownloads().Should().HaveCount(1);
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(default(RemoteEpisode));
Subject.Handle(new EpisodeInfoRefreshedEvent(remoteEpisode.Series, new List<Episode>(), new List<Episode>(), remoteEpisode.Episodes));
var trackedDownloads = Subject.GetTrackedDownloads();
trackedDownloads.Should().HaveCount(1);
trackedDownloads.First().RemoteEpisode.Should().BeNull();
}
[Test]
public void should_not_throw_when_processing_deleted_series()
{
GivenDownloadHistory();
var remoteEpisode = new RemoteEpisode
{
Series = new Series() { Id = 5 },
Episodes = new List<Episode> { new Episode { Id = 4 } },
ParsedEpisodeInfo = new ParsedEpisodeInfo()
{
SeriesTitle = "TV Series",
SeasonNumber = 1,
EpisodeNumbers = new[] { 1 }
},
MappedSeasonNumber = 0
};
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(default(RemoteEpisode));
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<EpisodeHistory>());
var client = new DownloadClientDefinition()
{
Id = 1,
Protocol = DownloadProtocol.Torrent
};
var item = new DownloadClientItem()
{
Title = "TV Series - S01E01",
DownloadId = "12345",
DownloadClientInfo = new DownloadClientItemClientInfo
{
Id = 1,
Type = "Blackhole",
Name = "Blackhole Client",
Protocol = DownloadProtocol.Torrent
}
};
Subject.TrackDownload(client, item);
Subject.GetTrackedDownloads().Should().HaveCount(1);
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), It.IsAny<int>(), null))
.Returns(default(RemoteEpisode));
Subject.Handle(new SeriesDeletedEvent(remoteEpisode.Series, true, true));
var trackedDownloads = Subject.GetTrackedDownloads();
trackedDownloads.Should().HaveCount(1);
trackedDownloads.First().RemoteEpisode.Should().BeNull();
}
}
}

View File

@@ -1,3 +1,4 @@
using System.IO;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -76,5 +77,37 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.Calculate(_series, _episodeFile).Should().Be(20);
}
[Test]
public void should_return_score_for_original_path_folder_name_if_highest()
{
var folderName = "folder-name";
var fileName = "file-name";
_episodeFile.OriginalFilePath = Path.Combine(folderName, fileName);
GivenPreferredWordScore(_episodeFile.RelativePath, 20);
GivenPreferredWordScore(_episodeFile.Path, 50);
GivenPreferredWordScore(folderName, 60);
GivenPreferredWordScore(fileName, 50);
Subject.Calculate(_series, _episodeFile).Should().Be(60);
}
[Test]
public void should_return_score_for_original_path_file_name_if_highest()
{
var folderName = "folder-name";
var fileName = "file-name";
_episodeFile.OriginalFilePath = Path.Combine(folderName, fileName);
GivenPreferredWordScore(_episodeFile.RelativePath, 20);
GivenPreferredWordScore(_episodeFile.Path, 50);
GivenPreferredWordScore(folderName, 40);
GivenPreferredWordScore(fileName, 50);
Subject.Calculate(_series, _episodeFile).Should().Be(50);
}
}
}

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
@@ -60,10 +60,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
FileEpisodeInfo = fileEpisodeInfo,
FolderEpisodeInfo = folderEpisodeInfo,
Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(),
Series = _series
Series = _series,
OtherVideoFiles = true
};
Subject.Aggregate(localEpisode, null, true);
Subject.Aggregate(localEpisode, null);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
@@ -82,7 +83,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
@@ -101,7 +102,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(folderEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(fileEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());
@@ -143,7 +144,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
.Setup(s => s.ParseSpecialEpisodeTitle(fileEpisodeInfo, It.IsAny<string>(), _series))
.Returns(specialEpisodeInfo);
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetEpisodes(specialEpisodeInfo, _series, localEpisode.SceneSource, null), Times.Once());

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
_localEpisode.FileEpisodeInfo = GetParsedEpisodeInfo(Language.English, _simpleReleaseTitle);
Subject.Aggregate(_localEpisode, null, false).Language.Should().Be(_localEpisode.FileEpisodeInfo.Language);
Subject.Aggregate(_localEpisode, null).Language.Should().Be(_localEpisode.FileEpisodeInfo.Language);
}
[Test]
@@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
_localEpisode.FolderEpisodeInfo = GetParsedEpisodeInfo(Language.English, _simpleReleaseTitle);
_localEpisode.FileEpisodeInfo = GetParsedEpisodeInfo(Language.English, _simpleReleaseTitle);
Subject.Aggregate(_localEpisode, null, false).Language.Should().Be(_localEpisode.FolderEpisodeInfo.Language);
Subject.Aggregate(_localEpisode, null).Language.Should().Be(_localEpisode.FolderEpisodeInfo.Language);
}
[Test]
@@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
_localEpisode.FolderEpisodeInfo = GetParsedEpisodeInfo(Language.English, _simpleReleaseTitle);
_localEpisode.FileEpisodeInfo = GetParsedEpisodeInfo(Language.English, _simpleReleaseTitle);
Subject.Aggregate(_localEpisode, null, false).Language.Should().Be(_localEpisode.DownloadClientEpisodeInfo.Language);
Subject.Aggregate(_localEpisode, null).Language.Should().Be(_localEpisode.DownloadClientEpisodeInfo.Language);
}
@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
_localEpisode.FolderEpisodeInfo = GetParsedEpisodeInfo(Language.English, _simpleReleaseTitle);
_localEpisode.FileEpisodeInfo = GetParsedEpisodeInfo(Language.French, _simpleReleaseTitle);
Subject.Aggregate(_localEpisode, null, false).Language.Should().Be(_localEpisode.FileEpisodeInfo.Language);
Subject.Aggregate(_localEpisode, null).Language.Should().Be(_localEpisode.FileEpisodeInfo.Language);
}
[Test]
@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
_localEpisode.Episodes.First().Title = "The Swedish Job";
_localEpisode.FileEpisodeInfo = GetParsedEpisodeInfo(Language.Swedish, "Series.Title.S01E01.The.Swedish.Job.720p.WEB-DL-RlsGrp");
Subject.Aggregate(_localEpisode, null, false).Language.Should().Be(Language.English);
Subject.Aggregate(_localEpisode, null).Language.Should().Be(Language.English);
}
@@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
_localEpisode.Episodes.First().Title = "The Swedish Job";
_localEpisode.FileEpisodeInfo = GetParsedEpisodeInfo(Language.French, "Series.Title.S01E01.The.Swedish.Job.720p.WEB-DL-RlsGrp");
Subject.Aggregate(_localEpisode, null, false).Language.Should().Be(_localEpisode.FileEpisodeInfo.Language);
Subject.Aggregate(_localEpisode, null).Language.Should().Be(_localEpisode.FileEpisodeInfo.Language);
}
}

View File

@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
GivenAugmenters(_fileExtensionAugmenter, nullMock);
var result = Subject.Aggregate(new LocalEpisode(), null, false);
var result = Subject.Aggregate(new LocalEpisode(), null);
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Extension);
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.Extension);
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
GivenAugmenters(_fileExtensionAugmenter, _nameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), null, false);
var result = Subject.Aggregate(new LocalEpisode(), null);
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name);
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.Name);
@@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
GivenAugmenters(_fileExtensionAugmenter, _mediaInfoAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), null, false);
var result = Subject.Aggregate(new LocalEpisode(), null);
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Extension);
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.MediaInfo);
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
GivenAugmenters(_nameAugmenter, _mediaInfoAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), null, false);
var result = Subject.Aggregate(new LocalEpisode(), null);
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name);
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.MediaInfo);
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem(), false);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem());
result.Quality.SourceDetectionSource.Should().Be(QualityDetectionSource.Name);
result.Quality.ResolutionDetectionSource.Should().Be(QualityDetectionSource.Name);
@@ -120,7 +120,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
{
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem(), false);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem());
result.Quality.Revision.Version.Should().Be(1);
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Unknown);
@@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem(), false);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem());
result.Quality.Revision.Version.Should().Be(2);
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name);
@@ -148,7 +148,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem(), false);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem());
result.Quality.Revision.Version.Should().Be(0);
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name);
@@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
GivenAugmenters(_nameAugmenter, _releaseNameAugmenter);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem(), false);
var result = Subject.Aggregate(new LocalEpisode(), new DownloadClientItem());
result.Quality.Revision.Version.Should().Be(2);
result.Quality.RevisionDetectionSource.Should().Be(QualityDetectionSource.Name);

View File

@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
localEpisode.ReleaseGroup.Should().Be("Wizzy");
}
@@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
localEpisode.ReleaseGroup.Should().Be("Wizzy");
}
@@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
localEpisode.ReleaseGroup.Should().Be("Viva");
}
@@ -92,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
localEpisode.ReleaseGroup.Should().Be("Drone");
}
@@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators
Series = _series
};
Subject.Aggregate(localEpisode, null, false);
Subject.Aggregate(localEpisode, null);
localEpisode.ReleaseGroup.Should().Be("Wizzy");
}

View File

@@ -0,0 +1,199 @@
using System.Collections.Generic;
using System.IO;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Profiles.Languages;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
[TestFixture]
public class GetSceneNameFixture : CoreTest
{
private LocalEpisode _localEpisode;
private string _seasonName = "series.title.s02.dvdrip.x264-ingot";
private string _episodeName = "series.title.s02e23.dvdrip.x264-ingot";
[SetUp]
public void Setup()
{
var series = Builder<Series>.CreateNew()
.With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() })
.With(l => l.LanguageProfile = new LanguageProfile
{
Cutoff = Language.Spanish,
Languages = Languages.LanguageFixture.GetDefaultLanguages()
})
.With(s => s.Path = @"C:\Test\TV\Series Title".AsOsAgnostic())
.Build();
var episode = Builder<Episode>.CreateNew()
.Build();
_localEpisode = new LocalEpisode
{
Series = series,
Episodes = new List<Episode> {episode},
Path = Path.Combine(series.Path, "Series Title - S02E23 - Episode Title.mkv"),
Quality = new QualityModel(Quality.Bluray720p),
ReleaseGroup = "DRONE"
};
}
private void GivenExistingFileOnDisk()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(new List<EpisodeFile>());
}
[Test]
public void should_use_download_client_item_title_as_scene_name()
{
_localEpisode.DownloadClientEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _episodeName
};
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.Be(_episodeName);
}
[Test]
public void should_not_use_download_client_item_title_as_scene_name_if_full_season()
{
_localEpisode.DownloadClientEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _seasonName,
FullSeason = true
};
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _seasonName, _episodeName)
.AsOsAgnostic();
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.BeNull();
}
[Test]
public void should_not_use_download_client_item_title_as_scene_name_if_there_are_other_video_files()
{
_localEpisode.OtherVideoFiles = true;
_localEpisode.DownloadClientEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _seasonName,
FullSeason = false
};
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _seasonName, _episodeName)
.AsOsAgnostic();
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.BeNull();
}
[Test]
public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename()
{
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _seasonName, _episodeName + ".mkv")
.AsOsAgnostic();
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.Be(_episodeName);
}
[Test]
public void should_not_use_file_name_as_scenename_if_it_doesnt_look_like_scenename()
{
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
.AsOsAgnostic();
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.BeNull();
}
[Test]
public void should_use_folder_name_as_scenename_only_if_it_looks_like_scenename()
{
_localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _episodeName
};
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.Be(_episodeName);
}
[Test]
public void should_not_use_folder_name_as_scenename_if_it_doesnt_look_like_scenename()
{
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
.AsOsAgnostic();
_localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = "aaaaa"
};
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.BeNull();
}
[Test]
public void should_not_use_folder_name_as_scenename_if_it_is_for_a_full_season()
{
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
.AsOsAgnostic();
_localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _seasonName,
FullSeason = true
};
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.BeNull();
}
[Test]
public void should_not_use_folder_name_as_scenename_if_there_are_other_video_files()
{
_localEpisode.OtherVideoFiles = true;
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
.AsOsAgnostic();
_localEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _seasonName,
FullSeason = false
};
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.BeNull();
}
[TestCase(".mkv")]
[TestCase(".par2")]
[TestCase(".nzb")]
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
{
_localEpisode.DownloadClientEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = _episodeName + extension
};
SceneNameCalculator.GetSceneName(_localEpisode).Should()
.Be(_episodeName);
}
}
}

View File

@@ -21,7 +21,7 @@ using NzbDrone.Test.Common;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Profiles.Languages;
namespace NzbDrone.Core.Test.MediaFiles
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
{
[TestFixture]
public class ImportApprovedEpisodesFixture : CoreTest<ImportApprovedEpisodes>
@@ -169,112 +169,6 @@ namespace NzbDrone.Core.Test.MediaFiles
Times.Never());
}
[Test]
public void should_use_nzb_title_as_scene_name()
{
GivenNewDownload();
_downloadClientItem.Title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot";
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == _downloadClientItem.Title)));
}
[TestCase(".mkv")]
[TestCase(".par2")]
[TestCase(".nzb")]
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
{
GivenNewDownload();
var title = "malcolm.in.the.middle.s02e05.dvdrip.xvid-ingot";
_downloadClientItem.Title = title + extension;
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == title)));
}
[Test]
public void should_not_use_nzb_title_as_scene_name_if_full_season()
{
GivenNewDownload();
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot.mkv");
_downloadClientItem.Title = "malcolm.in.the.middle.s02.dvdrip.xvid-ingot";
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, _downloadClientItem);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == "malcolm.in.the.middle.s02e23.dvdrip.xvid-ingot")));
}
[Test]
public void should_use_file_name_as_scenename_only_if_it_looks_like_scenename()
{
GivenNewDownload();
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "series.title.s02e23.dvdrip.xvid-ingot.mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == "series.title.s02e23.dvdrip.xvid-ingot")));
}
[Test]
public void should_not_use_file_name_as_scenename_if_it_doesnt_looks_like_scenename()
{
GivenNewDownload();
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == null)));
}
[Test]
public void should_use_folder_name_as_scenename_only_if_it_looks_like_scenename()
{
GivenNewDownload();
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
_approvedDecisions.First().LocalEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = "series.title.s02e23.dvdrip.xvid-ingot"
};
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == "series.title.s02e23.dvdrip.xvid-ingot")));
}
[Test]
public void should_not_use_folder_name_as_scenename_if_it_doesnt_looks_like_scenename()
{
GivenNewDownload();
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
_approvedDecisions.First().LocalEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = "aaaaa.mkv"
};
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == null)));
}
[Test]
public void should_not_use_folder_name_as_scenename_if_it_is_for_a_full_season()
{
GivenNewDownload();
_approvedDecisions.First().LocalEpisode.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), "aaaaa.mkv");
_approvedDecisions.First().LocalEpisode.FolderEpisodeInfo = new ParsedEpisodeInfo
{
ReleaseTitle = "series.title.s02.dvdrip.xvid-ingot.mkv",
FullSeason = true
};
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.SceneName == null)));
}
[Test]
public void should_import_larger_files_first()
{
@@ -483,5 +377,18 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IMediaFileService>().Verify(v => v.Add(It.Is<EpisodeFile>(c => c.OriginalFilePath == $"{name}\\subfolder\\{name}.mkv".AsOsAgnostic())));
}
[Test]
public void should_include_scene_name_with_new_downloads()
{
var firstDecision = _approvedDecisions.First();
firstDecision.LocalEpisode.SceneName = "Series.Title.S01E01.dvdrip-DRONE";
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeEpisodeFile(It.Is<EpisodeFile>(e => e.SceneName == firstDecision.LocalEpisode.SceneName), _approvedDecisions.First().LocalEpisode, false),
Times.Once());
}
}
}

View File

@@ -93,8 +93,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
private void GivenAugmentationSuccess()
{
Mocker.GetMock<IAggregationService>()
.Setup(s => s.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
.Callback<LocalEpisode, DownloadClientItem, bool>((localEpisode, downloadClientItem, otherFiles) =>
.Setup(s => s.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>()))
.Callback<LocalEpisode, DownloadClientItem>((localEpisode, downloadClientItem) =>
{
localEpisode.Episodes = _localEpisode.Episodes;
});
@@ -164,7 +164,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
GivenSpecifications(_pass1);
Mocker.GetMock<IAggregationService>()
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>()))
.Throws<TestException>();
_videoFiles = new List<string>
@@ -179,7 +179,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IAggregationService>()
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>()), Times.Exactly(_videoFiles.Count));
ExceptionVerification.ExpectedErrors(3);
}
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
var decisions = Subject.GetImportDecisions(_videoFiles, _series);
Mocker.GetMock<IAggregationService>()
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()), Times.Exactly(_videoFiles.Count));
.Verify(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>()), Times.Exactly(_videoFiles.Count));
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
@@ -210,7 +210,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
public void should_return_a_decision_when_exception_is_caught()
{
Mocker.GetMock<IAggregationService>()
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>(), It.IsAny<bool>()))
.Setup(c => c.Augment(It.IsAny<LocalEpisode>(), It.IsAny<DownloadClientItem>()))
.Throws<TestException>();
_videoFiles = new List<string>

View File

@@ -1,3 +1,4 @@
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
@@ -6,6 +7,7 @@ using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
@@ -33,6 +35,24 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.With(p => p.FullSeason = false)
.Build())
.Build();
}
private void GivenEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, int[] episodeNumbers)
{
var seasonNumber = parsedEpisodeInfo.SeasonNumber;
var episodes = episodeNumbers.Select(n =>
Builder<Episode>.CreateNew()
.With(e => e.Id = seasonNumber * 10 + n)
.With(e => e.SeasonNumber = seasonNumber)
.With(e => e.EpisodeNumber = n)
.Build()
).ToList();
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetEpisodes(parsedEpisodeInfo, It.IsAny<Series>(), true, null))
.Returns(episodes);
}
[Test]
@@ -68,6 +88,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new int[0];
_localEpisode.FolderEpisodeInfo.FullSeason = true;
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, new []{ 1, 2, 3, 4, 5 });
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
@@ -78,6 +101,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
@@ -88,16 +114,22 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_be_disregard_subfolder()
public void should_disregard_subfolder()
{
_localEpisode.FileEpisodeInfo.EpisodeNumbers = new[] { 5, 6 };
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic();
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
@@ -106,6 +138,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
{
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic();
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
@@ -116,6 +151,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01E01E02.720p.HDTV-Sonarr\S01E05E06.mkv".AsOsAgnostic();
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
}
@@ -129,6 +167,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.SeasonNumber = 1;
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01.720p.HDTV-Sonarr\S02E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
@@ -143,6 +184,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.SeasonNumber = 1;
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01.720p.HDTV-Sonarr\S02E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeFalse();
@@ -158,6 +202,9 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
_localEpisode.FolderEpisodeInfo.SeasonNumber = 1;
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
GivenEpisodes(_localEpisode.FileEpisodeInfo, _localEpisode.FileEpisodeInfo.EpisodeNumbers);
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01.720p.HDTV-Sonarr\S01E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
@@ -182,6 +229,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
}
};
GivenEpisodes(actualInfo, actualInfo.EpisodeNumbers);
Mocker.GetMock<IParsingService>()
.Setup(v => v.ParseSpecialEpisodeTitle(fileInfo, It.IsAny<string>(), 0, 0, null))
.Returns(actualInfo);
@@ -196,12 +245,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
[Test]
public void should_be_accepted_if_file_has_absolute_episode_number_and_folder_uses_standard()
{
_localEpisode.FileEpisodeInfo.SeasonNumber = 0;
_localEpisode.FileEpisodeInfo.SeasonNumber = 1;
_localEpisode.FileEpisodeInfo.AbsoluteEpisodeNumbers = new[] { 1 };
_localEpisode.FolderEpisodeInfo.SeasonNumber = 1;
_localEpisode.FolderEpisodeInfo.EpisodeNumbers = new[] { 1, 2 };
GivenEpisodes(_localEpisode.FileEpisodeInfo, new []{ 1 });
GivenEpisodes(_localEpisode.FolderEpisodeInfo, _localEpisode.FolderEpisodeInfo.EpisodeNumbers);
_localEpisode.Path = @"C:\Test\Unsorted\Series.Title.S01.720p.HDTV-Sonarr\S02E01.mkv".AsOsAgnostic();
Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();

View File

@@ -287,14 +287,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotPrefer);
Mocker.GetMock<IPreferredWordService>()
.Setup(s => s.Calculate(It.IsAny<Series>(), It.IsAny<string>(), 0))
.Returns(5);
Mocker.GetMock<IEpisodeFilePreferredWordCalculator>()
.Setup(s => s.Calculate(It.IsAny<Series>(), It.IsAny<EpisodeFile>()))
.Returns(10);
_localEpisode.PreferredWordScore = 5;
_localEpisode.Quality = new QualityModel(Quality.Bluray1080p);
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(1)
@@ -364,14 +361,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotPrefer);
Mocker.GetMock<IPreferredWordService>()
.Setup(s => s.Calculate(It.IsAny<Series>(), It.IsAny<string>(), 0))
.Returns(5);
Mocker.GetMock<IEpisodeFilePreferredWordCalculator>()
.Setup(s => s.Calculate(It.IsAny<Series>(), It.IsAny<EpisodeFile>()))
.Returns(1);
_localEpisode.PreferredWordScore = 5;
_localEpisode.Quality = new QualityModel(Quality.Bluray1080p);
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(1)
@@ -397,14 +391,11 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
.Setup(s => s.DownloadPropersAndRepacks)
.Returns(ProperDownloadTypes.DoNotPrefer);
Mocker.GetMock<IPreferredWordService>()
.Setup(s => s.Calculate(It.IsAny<Series>(), It.IsAny<string>(), 0))
.Returns(5);
Mocker.GetMock<IEpisodeFilePreferredWordCalculator>()
.Setup(s => s.Calculate(It.IsAny<Series>(), It.IsAny<EpisodeFile>()))
.Returns(5);
_localEpisode.PreferredWordScore = 5;
_localEpisode.Quality = new QualityModel(Quality.Bluray1080p);
_localEpisode.Episodes = Builder<Episode>.CreateListOfSize(1)

View File

@@ -8,20 +8,28 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestFixture]
public class FormatVideoDynamicRangeFixture : TestBase
{
[TestCase(8, "BT.601 NTSC", "BT.709", "")]
[TestCase(10, "BT.2020", "PQ", "HDR")]
[TestCase(8, "BT.2020", "PQ", "")]
[TestCase(10, "BT.601 NTSC", "PQ", "")]
[TestCase(10, "BT.2020", "BT.709", "")]
[TestCase(10, "BT.2020", "HLG", "HDR")]
public void should_format_video_dynamic_range(int bitDepth, string colourPrimaries, string transferCharacteristics, string expectedVideoDynamicRange)
[TestCase(8, "", "", "", "", "")]
[TestCase(8, "BT.601 NTSC", "BT.709", "", "", "")]
[TestCase(10, "BT.2020", "PQ", "", "", "HDR")]
[TestCase(8, "BT.2020", "PQ", "", "", "")]
[TestCase(10, "BT.601 NTSC", "PQ", "", "", "")]
[TestCase(10, "BT.2020", "BT.709", "", "", "")]
[TestCase(10, "BT.2020", "HLG", "", "", "HDR")]
[TestCase(10, "", "", "Dolby Vision", "", "HDR")]
[TestCase(10, "", "", "SMPTE ST 2086", "HDR10", "HDR")]
[TestCase(8, "", "", "Dolby Vision", "", "HDR")]
[TestCase(8, "", "", "SMPTE ST 2086", "HDR10", "HDR")]
[TestCase(10, "BT.2020", "PQ", "Dolby Vision / SMPTE ST 2086", "Blu-ray / HDR10", "HDR")]
public void should_format_video_dynamic_range(int bitDepth, string colourPrimaries, string transferCharacteristics, string hdrFormat, string hdrFormatCompatibility, string expectedVideoDynamicRange)
{
var mediaInfo = new MediaInfoModel
{
VideoBitDepth = bitDepth,
VideoColourPrimaries = colourPrimaries,
VideoTransferCharacteristics = transferCharacteristics,
SchemaRevision = 5
VideoHdrFormat = hdrFormat,
VideoHdrFormatCompatibility = hdrFormatCompatibility,
SchemaRevision = 7
};
MediaInfoFormatter.FormatVideoDynamicRange(mediaInfo).Should().Be(expectedVideoDynamicRange);

View File

@@ -65,6 +65,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
info.VideoColourPrimaries.Should().Be("BT.601 NTSC");
info.VideoTransferCharacteristics.Should().Be("BT.709");
info.AudioAdditionalFeatures.Should().BeOneOf("", "LC");
info.VideoHdrFormat.Should().BeEmpty();
info.VideoHdrFormatCompatibility.Should().BeEmpty();
}
@@ -107,6 +109,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
info.VideoColourPrimaries.Should().Be("BT.601 NTSC");
info.VideoTransferCharacteristics.Should().Be("BT.709");
info.AudioAdditionalFeatures.Should().BeOneOf("", "LC");
info.VideoHdrFormat.Should().BeEmpty();
info.VideoHdrFormatCompatibility.Should().BeEmpty();
}
[Test]

View File

@@ -0,0 +1,114 @@
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Notifications.Email;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.NotificationTests.EmailTests
{
[TestFixture]
public class EmailSettingsValidatorFixture : CoreTest<EmailSettingsValidator>
{
private EmailSettings _emailSettings;
private TestValidator<EmailSettings> _validator;
[SetUp]
public void Setup()
{
_validator = new TestValidator<EmailSettings>
{
v => v.RuleFor(s => s).SetValidator(Subject)
};
_emailSettings = Builder<EmailSettings>.CreateNew()
.With(s => s.Server = "someserver")
.With(s => s.Port = 567)
.With(s => s.RequireEncryption = true)
.With(s => s.From = "dont@email.me")
.With(s => s.To = new string[] { "dont@email.me" })
.Build();
}
[Test]
public void should_be_valid_if_all_settings_valid()
{
_validator.Validate(_emailSettings).IsValid.Should().BeTrue();
}
[Test]
public void should_not_be_valid_if_port_is_out_of_range()
{
_emailSettings.Port = 900000;
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_server_is_empty()
{
_emailSettings.Server = "";
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_from_is_empty()
{
_emailSettings.From = "";
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
[Ignore("Allowed coz some email servers allow arbitrary source, we probably need to support 'Name <email>' syntax")]
public void should_not_be_valid_if_from_is_invalid(string email)
{
_emailSettings.From = email;
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_to_is_invalid(string email)
{
_emailSettings.To = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_cc_is_invalid(string email)
{
_emailSettings.Cc = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[TestCase("sonarr")]
[TestCase("sonarr@sonarr")]
[TestCase("email.me")]
public void should_not_be_valid_if_bcc_is_invalid(string email)
{
_emailSettings.Bcc = new string[] { email };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
[Test]
public void should_not_be_valid_if_to_bcc_cc_are_all_empty()
{
_emailSettings.To = new string[] { };
_emailSettings.Cc = new string[] { };
_emailSettings.Bcc = new string[] { };
_validator.Validate(_emailSettings).IsValid.Should().BeFalse();
}
}
}

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using FluentValidation.Results;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
@@ -60,7 +62,7 @@ namespace NzbDrone.Core.Test.NotificationTests
TestLogger.Info("OnDownload was called");
}
public override void OnRename(Series series)
public override void OnRename(Series series, List<RenamedEpisodeFile> renamedFiles)
{
TestLogger.Info("OnRename was called");
}

View File

@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.NotificationTests
{
(Subject.Definition.Settings as SynologyIndexerSettings).UpdateLibrary = false;
Subject.OnRename(_series);
Subject.OnRename(_series, new List<RenamedEpisodeFile>());
Mocker.GetMock<ISynologyIndexerProxy>()
.Verify(v => v.UpdateFolder(_series.Path), Times.Never());
@@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.NotificationTests
[Test]
public void should_update_entire_series_folder_on_rename()
{
Subject.OnRename(_series);
Subject.OnRename(_series, new List<RenamedEpisodeFile>());
Mocker.GetMock<ISynologyIndexerProxy>()
.Verify(v => v.UpdateFolder(@"C:\Test\".AsOsAgnostic()), Times.Once());

View File

@@ -9,100 +9,100 @@ namespace NzbDrone.Core.Test.ParserTests
[TestFixture]
public class AbsoluteEpisodeNumberParserFixture : CoreTest
{
[TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "High School DxD", 7, 0, 0)]
[TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Working!!", 6, 0, 0)]
[TestCase("[Commie]_Senki_Zesshou_Symphogear_-_11_[65F220B4]", "Senki Zesshou Symphogear", 11, 0, 0)]
[TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Rinne no Lagrange", 12, 0, 0)]
[TestCase("[Commie]_Rinne_no_Lagrange_-_15_[E76552EA]", "Rinne no Lagrange", 15, 0, 0)]
[TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "Hunter X Hunter", 33, 0, 0)]
[TestCase("[HorribleSubs]_Fairy_Tail_-_145_[720p]", "Fairy Tail", 145, 0, 0)]
[TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "Tonari no Kaibutsu-kun", 13, 0, 0)]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Yes Pretty Cure 5 Go Go!", 31, 0, 0)]
[TestCase("[K-F] One Piece 214", "One Piece", 214, 0, 0)]
[TestCase("[K-F] One Piece S10E14 214", "One Piece", 214, 10, 14)]
[TestCase("[K-F] One Piece 10x14 214", "One Piece", 214, 10, 14)]
[TestCase("[K-F] One Piece 214 10x14", "One Piece", 214, 10, 14)]
// [TestCase("One Piece S10E14 214", "One Piece", 214, 10, 14)]
// [TestCase("One Piece 10x14 214", "One Piece", 214, 10, 14)]
// [TestCase("One Piece 214 10x14", "One Piece", 214, 10, 14)]
// [TestCase("214 One Piece 10x14", "One Piece", 214, 10, 14)]
[TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Bleach", 31, 0, 0)]
[TestCase("Bleach - 031 - The Resolution to Kill [Lunar]", "Bleach", 31, 0, 0)]
[TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "Hack Sign", 1, 0, 0)]
[TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)]
[TestCase("ducktales_e66_time_is_money_part_one_marking_time", "ducktales", 66, 0, 0)]
[TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0].mkv", "No Game No Life", 1, 0, 0)]
[TestCase("[FroZen] Miyuki - 23 [DVD][7F6170E6]", "Miyuki", 23, 0, 0)]
[TestCase("[Commie] Yowamushi Pedal - 32 [0BA19D5B]", "Yowamushi Pedal", 32, 0, 0)]
[TestCase("[Doki] Mahouka Koukou no Rettousei - 07 (1280x720 Hi10P AAC) [80AF7DDE]", "Mahouka Koukou no Rettousei", 7, 0, 0)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [480p]", "Yowamushi Pedal", 32, 0, 0)]
[TestCase("[CR] Sailor Moon - 004 [480p][48CE2D0F]", "Sailor Moon", 4, 0, 0)]
[TestCase("[Chibiki] Puchimas!! - 42 [360p][7A4FC77B]", "Puchimas!!", 42, 0, 0)]
[TestCase("[HorribleSubs] Yowamushi Pedal - 32 [1080p]", "Yowamushi Pedal", 32, 0, 0)]
[TestCase("[HorribleSubs] Love Live! S2 - 07 [720p]", "Love Live! S2", 7, 0, 0)]
[TestCase("[DeadFish] Onee-chan ga Kita - 09v2 [720p][AAC]", "Onee-chan ga Kita", 9, 0, 0)]
[TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0]", "No Game No Life", 1, 0, 0)]
[TestCase("[S-T-D] Soul Eater Not! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "Soul Eater Not!", 6, 0, 0)]
[TestCase("No Game No Life - 010 (720p) [27AAA0A0].mkv", "No Game No Life", 10, 0, 0)]
[TestCase("Initial D Fifth Stage - 01 DVD - Central Anime", "Initial D Fifth Stage", 1, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_01(DVD)_-_(Central_Anime)[5AF6F1E4].mkv", "Initial D Fifth Stage", 1, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_02(DVD)_-_(Central_Anime)[0CA65F00].mkv", "Initial D Fifth Stage", 2, 0, 0)]
[TestCase("Initial D Fifth Stage - 03 DVD - Central Anime", "Initial D Fifth Stage", 3, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_03(DVD)_-_(Central_Anime)[629BD592].mkv", "Initial D Fifth Stage", 3, 0, 0)]
[TestCase("Initial D Fifth Stage - 14 DVD - Central Anime", "Initial D Fifth Stage", 14, 0, 0)]
[TestCase("Initial_D_Fifth_Stage_-_14(DVD)_-_(Central_Anime)[0183D922].mkv", "Initial D Fifth Stage", 14, 0, 0)]
[TestCase("[SubDESU]_Show_One_07_(1280x720_x264-AAC)_[6B7FD717]", "Show One", 7, 0, 0)]
[TestCase("[Chihiro]_Show!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Show!!", 6, 0, 0)]
[TestCase("[Commie]_Some_Anime_Show_-_11_[65F220B4]", "Some Anime Show", 11, 0, 0)]
[TestCase("[Underwater]_Some_Anime_Show_-_12_(720p)_[5C7BC4F9]", "Some Anime Show", 12, 0, 0)]
[TestCase("[Commie]_Some_Anime_Show_-_15_[E76552EA]", "Some Anime Show", 15, 0, 0)]
[TestCase("[HorribleSubs]_Some_Anime_Show_-_33_[720p]", "Some Anime Show", 33, 0, 0)]
[TestCase("[HorribleSubs]_Some_Anime_Show_-_145_[720p]", "Some Anime Show", 145, 0, 0)]
[TestCase("[HorribleSubs] Some Anime Show - 13 [1080p].mkv", "Some Anime Show", 13, 0, 0)]
[TestCase("[Doremi].Some.Anime.Show.8.Go!.31.[1280x720].[C65D4B1F].mkv", "Some Anime Show 8 Go!", 31, 0, 0)]
[TestCase("[K-F] Some Anime Show 214", "Some Anime Show", 214, 0, 0)]
[TestCase("[K-F] Some Anime Show S10E14 214", "Some Anime Show", 214, 10, 14)]
[TestCase("[K-F] Some Anime Show 10x14 214", "Some Anime Show", 214, 10, 14)]
[TestCase("[K-F] Some Anime Show 214 10x14", "Some Anime Show", 214, 10, 14)]
[TestCase("Some Anime Show - 031 - The Resolution to Kill [Lunar].avi", "Some Anime Show", 31, 0, 0)]
[TestCase("Some Anime Show - 031 - The Resolution to Kill [Lunar]", "Some Anime Show", 31, 0, 0)]
[TestCase("[ACX]Some Anime Show 01 Role Play [Kosaka] [9C57891E].mkv", "Some Anime Show", 1, 0, 0)]
[TestCase("[SFW-sage] Some Anime Show S3 - 12 [720p][D07C91FC]", "Some Anime Show S3", 12, 0, 0)]
[TestCase("Some_Anime_Show_e66_time_is_money_part_one_marking_time", "Some Anime Show", 66, 0, 0)]
[TestCase("[Underwater-FFF] No Series Title No Life - 01 (720p) [27AAA0A0].mkv", "No Series Title No Life", 1, 0, 0)]
[TestCase("[FroZen] Series Title - 23 [DVD][7F6170E6]", "Series Title", 23, 0, 0)]
[TestCase("[Commie] Series Title - 32 [0BA19D5B]", "Series Title", 32, 0, 0)]
[TestCase("[Doki]Series Title - 07 (1280x720 Hi10P AAC) [80AF7DDE]", "Series Title", 7, 0, 0)]
[TestCase("[HorribleSubs] Series Title - 32 [480p]", "Series Title", 32, 0, 0)]
[TestCase("[CR] Series Title - 004 [480p][48CE2D0F]", "Series Title", 4, 0, 0)]
[TestCase("[Chibiki] Series Title!! - 42 [360p][7A4FC77B]", "Series Title!!", 42, 0, 0)]
[TestCase("[HorribleSubs] Series Title - 32 [1080p]", "Series Title", 32, 0, 0)]
[TestCase("[HorribleSubs] Series Title! S2 - 07 [720p]", "Series Title! S2", 7, 0, 0)]
[TestCase("[DeadFish] Series Title - 09v2 [720p][AAC]", "Series Title", 9, 0, 0)]
[TestCase("[Underwater-FFF] Series Title - 01 (720p) [27AAA0A0]", "Series Title", 1, 0, 0)]
[TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "Series Title!", 6, 0, 0)]
[TestCase("Series Title - 010 (720p) [27AAA0A0].mkv", "Series Title", 10, 0, 0)]
[TestCase("Initial_Series_Title - 01 DVD - Central Anime", "Initial Series Title", 1, 0, 0)]
[TestCase("Initial_Series_Title_-_01(DVD)_-_(Central_Anime)[5AF6F1E4].mkv", "Initial Series Title", 1, 0, 0)]
[TestCase("Initial_Series_Title_-_02(DVD)_-_(Central_Anime)[0CA65F00].mkv", "Initial Series Title", 2, 0, 0)]
[TestCase("Initial_Series_Title - 03 DVD - Central Anime", "Initial Series Title", 3, 0, 0)]
[TestCase("Initial_Series_Title_-_03(DVD)_-_(Central_Anime)[629BD592].mkv", "Initial Series Title", 3, 0, 0)]
[TestCase("Initial_Series_Title - 14 DVD - Central Anime", "Initial Series Title", 14, 0, 0)]
[TestCase("Initial_Series_Title_-_14(DVD)_-_(Central_Anime)[0183D922].mkv", "Initial Series Title", 14, 0, 0)]
// [TestCase("Initial D - 4th Stage Ep 01.mkv", "Initial D - 4th Stage", 1, 0, 0)]
[TestCase("[ChihiroDesuYo].No.Game.No.Life.-.09.1280x720.10bit.AAC.[24CCE81D]", "No Game No Life", 9, 0, 0)]
[TestCase("Fairy Tail - 001 - Fairy Tail", "Fairy Tail", 001, 0, 0)]
[TestCase("Fairy Tail - 049 - The Day of Fated Meeting", "Fairy Tail", 049, 0, 0)]
[TestCase("Fairy Tail - 050 - Special Request Watch Out for the Guy You Like!", "Fairy Tail", 050, 0, 0)]
[TestCase("Fairy Tail - 099 - Natsu vs. Gildarts", "Fairy Tail", 099, 0, 0)]
[TestCase("Fairy Tail - 100 - Mest", "Fairy Tail", 100, 0, 0)]
[TestCase("[ChihiroDesuYo].Series.Title.-.09.1280x720.10bit.AAC.[24CCE81D]", "Series Title", 9, 0, 0)]
[TestCase("Series Title - 001 - Fairy Tail", "Series Title", 001, 0, 0)]
[TestCase("Series Title - 049 - The Day of Fated Meeting", "Series Title", 049, 0, 0)]
[TestCase("Series Title - 050 - Special Request Watch Out for the Guy You Like!", "Series Title", 050, 0, 0)]
[TestCase("Series Title - 099 - Natsu vs. Gildarts", "Series Title", 099, 0, 0)]
[TestCase("Series Title - 100 - Mest", "Series Title", 100, 0, 0)]
// [TestCase("Fairy Tail - 101 - Mest", "Fairy Tail", 101, 0, 0)] //This gets caught up in the 'see' numbering
[TestCase("[Exiled-Destiny] Angel Beats Ep01 (D2201EC5).mkv", "Angel Beats", 1, 0, 0)]
[TestCase("[Commie] Nobunaga the Fool - 23 [5396CA24].mkv", "Nobunaga the Fool", 23, 0, 0)]
[TestCase("[FFF] Seikoku no Dragonar - 01 [1FB538B5].mkv", "Seikoku no Dragonar", 1, 0, 0)]
[TestCase("[Hatsuyuki]Fate_Zero-01[1280x720][122E6EF8]", "Fate Zero", 1, 0, 0)]
[TestCase("[CBM]_Monster_-_11_-_511_Kinderheim_[6C70C4E4].mkv", "Monster", 11, 0, 0)]
[TestCase("[HorribleSubs] Log Horizon 2 - 05 [720p].mkv", "Log Horizon 2", 5, 0, 0)]
[TestCase("[Commie] Log Horizon 2 - 05 [FCE4D070].mkv", "Log Horizon 2", 5, 0, 0)]
[TestCase("[Exiled-Destiny] Series Title Ep01 (D2201EC5).mkv", "Series Title", 1, 0, 0)]
[TestCase("[Commie] Series Title - 23 [5396CA24].mkv", "Series Title", 23, 0, 0)]
[TestCase("[FFF] Series Title - 01 [1FB538B5].mkv", "Series Title", 1, 0, 0)]
[TestCase("[Hatsuyuki]Series_Title-01[1280x720][122E6EF8]", "Series Title", 1, 0, 0)]
[TestCase("[CBM]_Series_Title_-_11_-_511_Kinderheim_[6C70C4E4].mkv", "Series Title", 11, 0, 0)]
[TestCase("[HorribleSubs] Series Title 2 - 05 [720p].mkv", "Series Title 2", 5, 0, 0)]
[TestCase("[Commie] Series Title 2 - 05 [FCE4D070].mkv", "Series Title 2", 5, 0, 0)]
[TestCase("[DRONE]Series.Title.100", "Series Title", 100, 0, 0)]
[TestCase("[RlsGrp]Series.Title.2010.S01E01.001.HDTV-720p.x264-DTS", "Series Title 2010", 1, 1, 1)]
[TestCase("Dragon Ball Kai - 130 - Found You, Gohan! Harsh Training in the Kaioshin Realm! [Baaro][720p][5A1AD35B].mkv", "Dragon Ball Kai", 130, 0, 0)]
[TestCase("Dragon Ball Kai - 131 - A Merged Super-Warrior Is Born, His Name Is Gotenks!! [Baaro][720p][32E03F96].mkv", "Dragon Ball Kai", 131, 0, 0)]
[TestCase("[HorribleSubs] Magic Kaito 1412 - 01 [1080p]", "Magic Kaito 1412", 1, 0, 0)]
[TestCase("[Jumonji-Giri]_[F-B]_Kagihime_Monogatari_Eikyuu_Alice_Rondo_Ep04_(0b0e2c10).mkv", "Kagihime Monogatari Eikyuu Alice Rondo", 4, 0, 0)]
[TestCase("[Jumonji-Giri]_[F-B]_Kagihime_Monogatari_Eikyuu_Alice_Rondo_Ep08_(8246e542).mkv", "Kagihime Monogatari Eikyuu Alice Rondo", 8, 0, 0)]
[TestCase("Knights of Sidonia - 01 [1080p 10b DTSHD-MA eng sub].mkv", "Knights of Sidonia", 1, 0, 0)]
[TestCase("Series Title - 130 - Found You, Gohan! Harsh Training in the Kaioshin Realm! [Baaro][720p][5A1AD35B].mkv", "Series Title", 130, 0, 0)]
[TestCase("Series Title - 131 - A Merged Super-Warrior Is Born, His Name Is Gotenks!! [Baaro][720p][32E03F96].mkv", "Series Title", 131, 0, 0)]
[TestCase("[HorribleSubs] Series Title - 01 [1080p]", "Series Title", 1, 0, 0)]
[TestCase("[Jumonji-Giri]_[F-B]_Series_Title_Ep04_(0b0e2c10).mkv", "Series Title", 4, 0, 0)]
[TestCase("[Jumonji-Giri]_[F-B]_Series_Title_Ep08_(8246e542).mkv", "Series Title", 8, 0, 0)]
[TestCase("Knights Series Title - 01 [1080p 10b DTSHD-MA eng sub].mkv", "Knights Series Title", 1, 0, 0)]
[TestCase("Series Title (2010) {01} Episode Title (1).hdtv-720p", "Series Title (2010)", 1, 0, 0)]
[TestCase("[HorribleSubs] Shirobako - 20 [720p].mkv", "Shirobako", 20, 0, 0)]
[TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 017 (115) [1280x720][B2CFBC0F]", "Dragon Ball Kai (2014)", 17, 0, 0)]
[TestCase("[Hatsuyuki] Dragon Ball Kai (2014) - 018 (116) [1280x720][C4A3B16E]", "Dragon Ball Kai (2014)", 18, 0, 0)]
[TestCase("Dragon Ball Kai (2014) - 39 (137) [v2][720p.HDTV][Unison Fansub]", "Dragon Ball Kai (2014)", 39, 0, 0)]
[TestCase("[HorribleSubs] Eyeshield 21 - 101 [480p].mkv", "Eyeshield 21", 101, 0, 0)]
[TestCase("[Cthuyuu].Taimadou.Gakuen.35.Shiken.Shoutai.-.03.[720p.H264.AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
//[TestCase("Taimadou.Gakuen.35.Shiken.Shoutai.-.03.(1280x720.HEVC.AAC)", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
[TestCase("[Cthuyuu] Taimadou Gakuen 35 Shiken Shoutai - 03 [720p H264 AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
[TestCase("Dragon Ball Super Episode 56 [VOSTFR V2][720p][AAC]-Mystic Z-Team", "Dragon Ball Super", 56, 0, 0)]
[TestCase("[Mystic Z-Team] Dragon Ball Super Episode 69 [VOSTFR_Finale][1080p][AAC].mp4", "Dragon Ball Super", 69, 0, 0)]
[TestCase("[Shark-Raws] Crayon Shin-chan #957 (NBN 1280x720 x264 AAC).mp4", "Crayon Shin-chan", 957, 0, 0)]
[TestCase("Love Rerun EP06 720p x265 AOZ.mp4", "Love Rerun", 6, 0, 0)]
[TestCase("Love Rerun 2018 EP06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
[TestCase("Love Rerun 2018 06 720p x265 AOZ.mp4", "Love Rerun 2018", 6, 0, 0)]
[TestCase("Boku No Hero Academia S03 - EP14 VOSTFR [1080p] [HardSub] Yass'Kun", "Boku No Hero Academia S03", 14, 0, 0)]
[TestCase("Boku No Hero Academia S3 - 15 VOSTFR [720p]", "Boku No Hero Academia S3", 15, 0, 0)]
[TestCase("Tokyo Ghoul: RE S2 - Episode 4 VOSTFR (1080p)", "Tokyo Ghoul RE S2", 4, 0, 0)]
[TestCase("To Aru Majutsu no Index III - Episode 5 VOSTFR (1080p)", "To Aru Majutsu no Index III", 5, 0, 0)]
[TestCase("[Prout] Steins;Gate 0 - Episode 5 VOSTFR (BDRip 1920x1080 x264 FLAC)", "Steins;Gate 0", 5, 0, 0)]
[TestCase("[BakedFish] Nakanohito Genome [Jikkyouchuu] - 01 [720p][AAC].mp4", "Nakanohito Genome [Jikkyouchuu]", 1, 0, 0)]
[TestCase("[HorribleSubs] Series Title - 20 [720p].mkv", "Series Title", 20, 0, 0)]
[TestCase("[Hatsuyuki] Series Title (2014) - 017 (115) [1280x720][B2CFBC0F]", "Series Title (2014)", 17, 0, 0)]
[TestCase("[Hatsuyuki] Series Title (2014) - 018 (116) [1280x720][C4A3B16E]", "Series Title (2014)", 18, 0, 0)]
[TestCase("Series Title (2014) - 39 (137) [v2][720p.HDTV][Unison Fansub]", "Series Title (2014)", 39, 0, 0)]
[TestCase("[HorribleSubs] Series Title 21 - 101 [480p].mkv", "Series Title 21", 101, 0, 0)]
[TestCase("[Cthuyuu].Series.Title.-.03.[720p.H264.AAC][8AD82C3A]", "Series Title", 3, 0, 0)]
//[TestCase("Series.Title.-.03.(1280x720.HEVC.AAC)", "Series Title", 3, 0, 0)]
[TestCase("[Cthuyuu] Series Title - 03 [720p H264 AAC][8AD82C3A]", "Series Title", 3, 0, 0)]
[TestCase("Series Title Episode 56 [VOSTFR V2][720p][AAC]-Mystic Z-Team", "Series Title", 56, 0, 0)]
[TestCase("[Mystic Z-Team] Series Title Episode 69 [VOSTFR_Finale][1080p][AAC].mp4", "Series Title", 69, 0, 0)]
[TestCase("[Shark-Raws] Series Title #957 (NBN 1280x720 x264 AAC).mp4", "Series Title", 957, 0, 0)]
[TestCase("Series Title EP06 720p x265 AOZ.mp4", "Series Title", 6, 0, 0)]
[TestCase("Series Title 2018 EP06 720p x265 AOZ.mp4", "Series Title 2018", 6, 0, 0)]
[TestCase("Series Title 2018 06 720p x265 AOZ.mp4", "Series Title 2018", 6, 0, 0)]
[TestCase("Series Title S03 - EP14 VOSTFR [1080p] [HardSub] Yass'Kun", "Series Title S03", 14, 0, 0)]
[TestCase("Series Title S3 - 15 VOSTFR [720p]", "Series Title S3", 15, 0, 0)]
[TestCase("A Series: RE S2 - Episode 4 VOSTFR (1080p)", "A Series: RE S2", 4, 0, 0)]
[TestCase("To Another Series III - Episode 5 VOSTFR (1080p)", "To Another Series III", 5, 0, 0)]
[TestCase("[Prout] Show;Title 0 - Episode 5 VOSTFR (BDRip 1920x1080 x264 FLAC)", "Show;Title 0", 5, 0, 0)]
[TestCase("[BakedFish] Some Show [Anime] - 01 [720p][AAC].mp4", "Some Show [Anime]", 1, 0, 0)]
[TestCase("Abc x Abc (2011) - 141 - Magician [KaiDubs] [1080p]", "Abc x Abc (2011)", 141, 0, 0)]
[TestCase("Abc Abc 484 VOSTFR par Abc-Abc (1280*720) - version MQ", "Abc Abc", 484, 0, 0)]
[TestCase("Abc - Abc Abc Abc - 107 VOSTFR par Fansub-Miracle Sharingan (1920x1080) - HQ_Draft", "Abc - Abc Abc Abc", 107, 0, 0)]
[TestCase("Abc Abc Abc Abc Episode 10 VOSTFR (1920x1080) Miracle Sharingan Fansub.MKV - Team - (À suivre)", "Abc Abc Abc Abc", 10, 0, 0)]
[TestCase("[Glenn] Ani ni Tsukeru Kusuri wa Nai! 3 - 11 (1080p AAC)[C34B2B3B].mkv", "Ani ni Tsukeru Kusuri wa Nai! 3", 11, 0, 0)]
[TestCase("Tatort.E1135.Lasst.den.Mond.am.Himmel.stehen.GERMAN.1080p.WEBRip.x264-Group", "Tatort", 1135, 0, 0)]
[TestCase("[HorribleSubs] Abc 100 - 07 [1080p].mkv", "Abc 100", 7, 0, 0)]
[TestCase("[HorribleSubs] Abc 100 S2 - 07 [1080p].mkv", "Abc 100 S2", 7, 0, 0)]
[TestCase("[Glenn] Series! 3 - 11 (1080p AAC)[C34B2B3B].mkv", "Series! 3", 11, 0, 0)]
[TestCase("SeriesTitle.E1135.Lasst.den.Mond.am.Himmel.stehen.GERMAN.1080p.WEBRip.x264-Group", "SeriesTitle", 1135, 0, 0)]
[TestCase("[HorribleSubs] Series 100 - 07 [1080p].mkv", "Series 100", 7, 0, 0)]
[TestCase("[HorribleSubs] Series 100 S2 - 07 [1080p].mkv", "Series 100 S2", 7, 0, 0)]
[TestCase("[abc] Adventure Series: 30 [Web][MKV][h264][720p][AAC 2.0][abc]", "Adventure Series:", 30, 0, 0)]
[TestCase("[XKsub] Series Title S2 [05][HEVC-10bit 1080p AAC][CHS&CHT&JPN]", "Series Title S2", 5, 0, 0)]
[TestCase("[Cheetah-Raws] Super Long Anime - 1000 (YTV 1280x720 x264 AAC)", "Super Long Anime", 1000, 0, 0)]
[TestCase("[DameDesuYo] ReZero kara Hajimeru Isekai Seikatsu (Season 2) - 33 (1280x720 10bit EAC3) [42A12A76].mkv", "ReZero kara Hajimeru Isekai Seikatsu", 33, 2, 0)]
//[TestCase("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{
@@ -115,9 +115,9 @@ namespace NzbDrone.Core.Test.ParserTests
result.FullSeason.Should().BeFalse();
}
[TestCase("[DeadFish] Kenzen Robo Daimidaler - 01 - Special [BD][720p][AAC]", "Kenzen Robo Daimidaler", 1)]
[TestCase("[DeadFish] Kenzen Robo Daimidaler - 01 - OVA [BD][720p][AAC]", "Kenzen Robo Daimidaler", 1)]
[TestCase("[DeadFish] Kenzen Robo Daimidaler - 01 - OVD [BD][720p][AAC]", "Kenzen Robo Daimidaler", 1)]
[TestCase("[DeadFish] Another Anime Show - 01 - Special [BD][720p][AAC]", "Another Anime Show", 1)]
[TestCase("[DeadFish] Another Anime Show - 01 - OVA [BD][720p][AAC]", "Another Anime Show", 1)]
[TestCase("[DeadFish] Another Anime Show - 01 - OVD [BD][720p][AAC]", "Another Anime Show", 1)]
public void should_parse_absolute_specials(string postTitle, string title, int absoluteEpisodeNumber)
{
var result = Parser.Parser.ParseTitle(postTitle);
@@ -130,19 +130,21 @@ namespace NzbDrone.Core.Test.ParserTests
result.Special.Should().BeTrue();
}
[TestCase("[ANBU-AonE]_Naruto_26-27_[F224EF26].avi", "Naruto", 26, 27)]
[TestCase("[Doutei] Recently, My Sister is Unusual - 01-12 [BD][720p-AAC]", "Recently, My Sister is Unusual", 1, 12)]
[TestCase("[ANBU-AonE]_SeriesTitle_26-27_[F224EF26].avi", "SeriesTitle", 26, 27)]
[TestCase("[Doutei] Some Good, Anime Show - 01-12 [BD][720p-AAC]", "Some Good, Anime Show", 1, 12)]
[TestCase("Series Title (2010) - 01-02-03 - Episode Title (1) HDTV-720p", "Series Title (2010)", 1, 3)]
[TestCase("[RlsGrp] Series Title (2010) - S01E01-02-03 - 001-002-003 - Episode Title HDTV-720p v2", "Series Title (2010)", 1, 3)]
[TestCase("[RlsGrp] Series Title (2010) - S01E01-02 - 001-002 - Episode Title HDTV-720p v2", "Series Title (2010)", 1, 2)]
[TestCase("Series Title (2010) - S01E01-02 (001-002) - Episode Title (1) HDTV-720p v2 [RlsGrp]", "Series Title (2010)", 1, 2)]
[TestCase("[HorribleSubs] Haikyuu!! (01-25) [1080p] (Batch)", "Haikyuu!!", 1, 25)]
[TestCase("Hunter X Hunter (2011) Episode 99-100 [1080p] [Dual.Audio] [x265]", "Hunter X Hunter (2011)", 99, 100)]
[TestCase("Twin Star Exorcists 1-13 (English Dub) [720p]", "Twin Star Exorcists", 1, 13)]
[TestCase("[HorribleSubs] Some Anime Show!! (01-25) [1080p] (Batch)", "Some Anime Show!!", 1, 25)]
[TestCase("Some Anime Show (2011) Episode 99-100 [1080p] [Dual.Audio] [x265]", "Some Anime Show (2011)", 99, 100)]
[TestCase("Some Anime Show 1-13 (English Dub) [720p]", "Some Anime Show", 1, 13)]
[TestCase("Series.Title.Ep01-12.Complete.English.AC3.DL.1080p.BluRay.x264", "Series Title", 1, 12)]
[TestCase("[Judas] Black Clover 091-123 [1080p][HEVC x265 10bit][Dual-Audio][Multi-Subs]", "Black Clover", 91, 123 )]
[TestCase("[Judas] Black Clover - 091-123 [1080p][HEVC x265 10bit][Dual-Audio][Multi-Subs]", "Black Clover", 91, 123)]
[TestCase("[HorribleSubs] Black Clover 01 - 119 [1080p] [Batch]", "Black Clover", 1, 119)]
[TestCase("[Judas] Some Anime Show 091-123 [1080p][HEVC x265 10bit][Dual-Audio][Multi-Subs]", "Some Anime Show", 91, 123 )]
[TestCase("[Judas] Some Anime Show - 091-123 [1080p][HEVC x265 10bit][Dual-Audio][Multi-Subs]", "Some Anime Show", 91, 123)]
[TestCase("[HorribleSubs] Some Anime Show 01 - 119 [1080p] [Batch]", "Some Anime Show", 1, 119)]
[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("", "", 1, 2)]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber)
{
@@ -155,7 +157,7 @@ namespace NzbDrone.Core.Test.ParserTests
result.FullSeason.Should().BeFalse();
}
[TestCase("[Vivid] Living Sky Saga S01 [Web][MKV][h264 10-bit][1080p][AAC 2.0]", "Living Sky Saga", 1)]
[TestCase("[Vivid] Some Anime Show S01 [Web][MKV][h264 10-bit][1080p][AAC 2.0]", "Some Anime Show", 1)]
public void should_parse_anime_season_packs(string postTitle, string title, int seasonNumber)
{
var result = Parser.Parser.ParseTitle(postTitle);
@@ -166,7 +168,7 @@ namespace NzbDrone.Core.Test.ParserTests
result.SeasonNumber.Should().Be(seasonNumber);
}
[TestCase("[HorribleSubs] Goblin Slayer - 10.5 [1080p].mkv", "Goblin Slayer", 10.5)]
[TestCase("[HorribleSubs] Show Slayer - 10.5 [1080p].mkv", "Show Slayer", 10.5)]
public void should_handle_anime_recap_numbering(string postTitle, string title, double specialEpisodeNumber)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -8,21 +8,21 @@ namespace NzbDrone.Core.Test.ParserTests
[TestFixture]
public class AnimeMetadataParserFixture : CoreTest
{
[TestCase("[SubDESU]_High_School_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "SubDESU", "6B7FD717")]
[TestCase("[Chihiro]_Working!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Chihiro", "859EEAFA")]
[TestCase("[Underwater]_Rinne_no_Lagrange_-_12_(720p)_[5C7BC4F9]", "Underwater", "5C7BC4F9")]
[TestCase("[HorribleSubs]_Hunter_X_Hunter_-_33_[720p]", "HorribleSubs", "")]
[TestCase("[HorribleSubs] Tonari no Kaibutsu-kun - 13 [1080p].mkv", "HorribleSubs", "")]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Doremi", "C65D4B1F")]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].[C65D4B1F]", "Doremi", "C65D4B1F")]
[TestCase("[Doremi].Yes.Pretty.Cure.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")]
[TestCase("[K-F] One Piece 214", "K-F", "")]
[TestCase("[K-F] One Piece S10E14 214", "K-F", "")]
[TestCase("[K-F] One Piece 10x14 214", "K-F", "")]
[TestCase("[K-F] One Piece 214 10x14", "K-F", "")]
[TestCase("Bleach - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")]
[TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")]
[TestCase("[S-T-D] Soul Eater Not! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")]
[TestCase("[SubDESU]_Show_Title_DxD_07_(1280x720_x264-AAC)_[6B7FD717]", "SubDESU", "6B7FD717")]
[TestCase("[Chihiro]_Show_Title!!_-_06_[848x480_H.264_AAC][859EEAFA]", "Chihiro", "859EEAFA")]
[TestCase("[Underwater]_Show_Title_-_12_(720p)_[5C7BC4F9]", "Underwater", "5C7BC4F9")]
[TestCase("[HorribleSubs]_Show_Title_-_33_[720p]", "HorribleSubs", "")]
[TestCase("[HorribleSubs] Show-Title - 13 [1080p].mkv", "HorribleSubs", "")]
[TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].[C65D4B1F].mkv", "Doremi", "C65D4B1F")]
[TestCase("[Doremi].Show.Title.5.Go.Go!.31[1280x720].[C65D4B1F]", "Doremi", "C65D4B1F")]
[TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")]
[TestCase("[K-F] Series Title 214", "K-F", "")]
[TestCase("[K-F] Series Title S10E14 214", "K-F", "")]
[TestCase("[K-F] Series Title 10x14 214", "K-F", "")]
[TestCase("[K-F] Series Title 214 10x14", "K-F", "")]
[TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")]
[TestCase("[ACX]Series Title 01 Episode Name [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")]
[TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")]
public void should_parse_absolute_numbers(string postTitle, string subGroup, string hash)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -86,13 +86,13 @@ namespace NzbDrone.Core.Test.ParserTests
success.Should().Be(repetitions);
}
[TestCase("thebiggestloser1618finale")]
[TestCase("theseriestitle1618finale")]
public void should_not_parse_file_name_without_proper_spacing(string fileName)
{
Parser.Parser.ParseTitle(fileName).Should().BeNull();
}
[TestCase("Big Forest (2018) Complete 360p HDTV AAC H.264-NEXT")]
[TestCase("Series Title (2018) Complete 360p HDTV AAC H.264-NEXT")]
public void should_not_parse_invalid_release_name(string fileName)
{
Parser.Parser.ParseTitle(fileName).Should().BeNull();

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