mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Compare commits
100 Commits
phantom-rt
...
v3.0.6.119
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a200dd5f6d | ||
|
|
f57efd30b8 | ||
|
|
f2f1039c5e | ||
|
|
12ba4f73ed | ||
|
|
370280b4bf | ||
|
|
6c505937da | ||
|
|
7272c5b7fc | ||
|
|
1d06c3fc15 | ||
|
|
ec9f62285a | ||
|
|
aae0d1c4ba | ||
|
|
652d44722b | ||
|
|
5a69801877 | ||
|
|
fa8b2f48e7 | ||
|
|
34faa417c1 | ||
|
|
d6c0635a26 | ||
|
|
d4167d7169 | ||
|
|
eea6be459d | ||
|
|
67e97f7aee | ||
|
|
37e1c4f2eb | ||
|
|
a9b8ec3505 | ||
|
|
f57cf1561b | ||
|
|
6672650b6b | ||
|
|
e4a064a1c0 | ||
|
|
a848e575cd | ||
|
|
01995e686d | ||
|
|
32058f1705 | ||
|
|
af3696af08 | ||
|
|
1477356cfc | ||
|
|
aa19ddfbfd | ||
|
|
a697a69e88 | ||
|
|
3abb7e156a | ||
|
|
240791a7cd | ||
|
|
0fe2453962 | ||
|
|
85f4cbe94c | ||
|
|
e1f7bce14b | ||
|
|
675c72f02e | ||
|
|
6619350f87 | ||
|
|
efd9fe9ad0 | ||
|
|
ab502ffda4 | ||
|
|
90697d77a5 | ||
|
|
fa7aa05d60 | ||
|
|
4ed5fefcc6 | ||
|
|
4c324fbbbf | ||
|
|
7da02c236a | ||
|
|
79cfa3a5f6 | ||
|
|
2746556ae2 | ||
|
|
d668e923af | ||
|
|
24ca47356e | ||
|
|
ab4f57f2fa | ||
|
|
13ff2d4c70 | ||
|
|
2728bf79ca | ||
|
|
cd28af98ee | ||
|
|
e9818b9982 | ||
|
|
d6cf370bcd | ||
|
|
cb8ed74fe9 | ||
|
|
4e81b33006 | ||
|
|
e67864fecb | ||
|
|
e289c428c6 | ||
|
|
23047623ee | ||
|
|
062e47e27e | ||
|
|
28ba037630 | ||
|
|
82da38941e | ||
|
|
10c770b116 | ||
|
|
3c45349404 | ||
|
|
b815d27a10 | ||
|
|
ec698c2cf7 | ||
|
|
e7d57a95f2 | ||
|
|
1250d71e80 | ||
|
|
e42d1af5ff | ||
|
|
88ad6f9544 | ||
|
|
54c386dd22 | ||
|
|
694360457d | ||
|
|
ae196af2ad | ||
|
|
12fafb2457 | ||
|
|
795bc91d25 | ||
|
|
044342f677 | ||
|
|
5960035d5d | ||
|
|
6c324c8a1c | ||
|
|
54a267d860 | ||
|
|
b5f08a8f06 | ||
|
|
653db8290e | ||
|
|
cbc4295f28 | ||
|
|
8876c9194d | ||
|
|
d898f55660 | ||
|
|
a85979c2f6 | ||
|
|
ae63373b2b | ||
|
|
044cb563a6 | ||
|
|
5302ee05bc | ||
|
|
29bc660cfb | ||
|
|
f8b8afdaa2 | ||
|
|
33b708927c | ||
|
|
42d9e37e7d | ||
|
|
fc3bea370f | ||
|
|
a1ddcf2b7b | ||
|
|
5b98a17873 | ||
|
|
8fd4adbdb6 | ||
|
|
952a7248c9 | ||
|
|
577604fccc | ||
|
|
3d3cd8cf5c | ||
|
|
5e4c9dfe60 |
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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-->
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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. -->
|
||||
@@ -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`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
fromdos ./debian/*
|
||||
chmod ugo-x ./debian/*
|
||||
cp -r ./debian ./debian_backup
|
||||
|
||||
BuildVersion=${dependent_build_number:-3.10.0.999}
|
||||
|
||||
@@ -1 +1 @@
|
||||
8
|
||||
10
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
ignores msbuild
|
||||
ignores libmediainfo0v5
|
||||
ignores libc6
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.noMessages {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,4 +54,8 @@
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ class EnhancedSelectInputOption extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
onPress = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const {
|
||||
id,
|
||||
onSelect
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -27,3 +27,9 @@
|
||||
color: $darkGray;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.isMissing {
|
||||
margin-left: 15px;
|
||||
color: $dangerColor;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.loadingMessage {
|
||||
margin: 50px 10px 0;
|
||||
margin: 10px 10px 0;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
&.inverse {
|
||||
background-color: $themeDarkColor;
|
||||
box-shadow: 0 5px 10px $popoverShadowInverseColor;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -26,7 +26,7 @@ function SeriesAlternateTitles({ alternateTitles }) {
|
||||
}
|
||||
|
||||
SeriesAlternateTitles.propTypes = {
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default SeriesAlternateTitles;
|
||||
|
||||
@@ -119,6 +119,13 @@
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.fileCountMessage {
|
||||
padding: 5px;
|
||||
white-space: nowrap;
|
||||
font-weight: 300;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.path,
|
||||
.sizeOnDisk,
|
||||
.qualityProfileName,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -131,6 +131,7 @@ function EditImportListModalContent(props) {
|
||||
name="rootFolderPath"
|
||||
helpText={'Root Folder list items will be added to'}
|
||||
{...rootFolderPath}
|
||||
includeMissingValue={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -9,3 +9,12 @@
|
||||
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.triggers {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.triggerEvents {
|
||||
margin-top: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -9,3 +9,11 @@
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -37,6 +37,8 @@ export const defaultState = {
|
||||
languageProfileId: 0,
|
||||
seriesType: seriesTypes.STANDARD,
|
||||
seasonFolder: true,
|
||||
searchForMissingEpisodes: false,
|
||||
searchForCutoffUnmetEpisodes: false,
|
||||
tags: []
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -63,6 +63,8 @@ export const defaultState = {
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'interactiveImport.sortKey',
|
||||
'interactiveImport.sortDirection',
|
||||
'interactiveImport.recentFolders',
|
||||
'interactiveImport.importMode'
|
||||
];
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
//
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
16
frontend/src/Utilities/Number/formatPreferredWordScore.js
Normal file
16
frontend/src/Utilities/Number/formatPreferredWordScore.js
Normal 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;
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user