1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-19 21:46:43 -04:00

Compare commits

...

56 Commits

Author SHA1 Message Date
Mark McDowall 143ccb1e2a Remove seriesTitle from EpisodeResource
Closes #6841
2024-06-28 06:22:10 -07:00
Mark McDowall 29480d9544 Fixed: Don't use cleaned up release title for release title 2024-06-28 06:22:04 -07:00
Mark McDowall 6de536a7ad Fixed: Limit Queue maximum page size to 200
Closes #6899
2024-06-26 09:45:43 -07:00
Mark McDowall bce848facf Fixed: Reprocessing items that were previously blocked during importing 2024-06-26 09:45:28 -07:00
Mark McDowall ea4fe392a0 New: Remove websites in parentheses before parsing 2024-06-25 15:52:24 -07:00
Mark McDowall 45fe585944 Fixed: Prevent errors parsing releases in unexpected formats 2024-06-25 15:52:24 -07:00
Mark McDowall a0d2933134 New: Ignore Deluge torrents without a title
Closes #6885
2024-06-25 18:52:12 -04:00
Mark McDowall 4c622fd412 New: Ability to select Plex Media Server from plex.tv
Closes #6887
2024-06-25 15:51:57 -07:00
Bogdan fb060730c7 Fixed: Exclude invalid releases from Newznab and Torznab parsers 2024-06-25 15:51:41 -07:00
Mark McDowall 6d5ff9c4d6 New: Improve UI status when downloads cannot be imported automatically
Closes #6873
2024-06-25 18:51:20 -04:00
Mark McDowall 63bed3e670 New: Parse anime seasons with trailing number in title
Closes #6883
2024-06-25 15:51:03 -07:00
Weblate e684c10432 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-06-25 15:50:56 -07:00
Bogdan d2509798e9 New: Display stats for delete multiple series modal 2024-06-17 20:40:04 -07:00
Bogdan 6c39855ebe Fix UpdatePackageProviderFixture for v4
ignore-downstream
2024-06-17 20:39:33 -07:00
Bogdan a30e9da767 New: Ignore inaccessible folders when getting folders 2024-06-17 20:39:33 -07:00
Mark McDowall f8e81396d4 Fixed: Importing from IMDb list 2024-06-17 20:39:22 -07:00
Mark McDowall 7fccf590a8 Fixed: Adding series with unknown items in queue 2024-06-17 23:39:13 -04:00
Stephan Sundermann e1b937e8d5 New: Add TMDB ID support
Closes #6866
2024-06-17 23:38:41 -04:00
Bogdan c331c8bd11 Ignore Grabbed from API docs
Run application in docs.sh specific to platform
2024-06-10 20:30:26 -07:00
Mark McDowall 52b72925f9 Fixed: Improve error messaging if config file isn't formatted correctly
Closes #6860
2024-06-10 20:30:13 -07:00
Mark McDowall 378fedcd9d Fixed: Skip invalid series paths during validation 2024-06-10 23:30:03 -04:00
Bogdan a90ab1a8fd Fixed: Ignore case when resolving indexer by name in release push 2024-06-10 20:29:46 -07:00
Bogdan 0edc5ba99a Fixed: Ignore case for name validation in providers 2024-06-10 20:29:46 -07:00
Bogdan ea54ade9bf New: Refresh cache for tracked queue on series add 2024-06-10 20:29:41 -07:00
Weblate e07eb05e8b Multiple Translations updated by Weblate
ignore-downstream

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

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

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

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

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

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

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-05-09 22:03:24 -07:00
209 changed files with 2481 additions and 1090 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0 FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4 SONARR_MAJOR_VERSION: 4
VERSION: 4.0.4 VERSION: 4.0.5
jobs: jobs:
backend: backend:
@@ -48,7 +48,7 @@ jobs:
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV" echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV" echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
echo "BRANCH=${RAW_BRANCH_NAME/\//-}" >> "$GITHUB_ENV" echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT" echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT" echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
+1
View File
@@ -8,5 +8,6 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'Sonarr/Sonarr'
steps: steps:
- uses: actions/labeler@v5 - uses: actions/labeler@v5
+1
View File
@@ -8,6 +8,7 @@ on:
jobs: jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'Sonarr/Sonarr'
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v5
with: with:
+9 -3
View File
@@ -25,17 +25,23 @@ slnFile=src/Sonarr.sln
platform=Posix platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Sonarr.Console.dll
else
application=Sonarr.dll
fi
dotnet clean $slnFile -c Debug dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest dotnet new tool-manifest
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 & dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
sleep 30 sleep 45
kill %1 kill %1
+1
View File
@@ -217,6 +217,7 @@ class Queue extends Component {
> >
<TableOptionsModalWrapper <TableOptionsModalWrapper
columns={columns} columns={columns}
maxPageSize={200}
{...otherProps} {...otherProps}
optionsComponent={QueueOptionsConnector} optionsComponent={QueueOptionsConnector}
> >
@@ -70,6 +70,11 @@ function QueueStatus(props) {
iconName = icons.DOWNLOADED; iconName = icons.DOWNLOADED;
title = translate('Downloaded'); title = translate('Downloaded');
if (trackedDownloadState === 'importBlocked') {
title += ` - ${translate('UnableToImportAutomatically')}`;
iconKind = kinds.WARNING;
}
if (trackedDownloadState === 'importPending') { if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`; title += ` - ${translate('WaitingToImport')}`;
iconKind = kinds.PURPLE; iconKind = kinds.PURPLE;
+10 -2
View File
@@ -24,7 +24,11 @@ function TimeleftCell(props) {
} = props; } = props;
if (status === 'delay') { if (status === 'delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
@@ -40,7 +44,11 @@ function TimeleftCell(props) {
} }
if (status === 'downloadClientUnavailable') { if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
+1 -1
View File
@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
if (view === calendarViews.WEEK) { if (view === calendarViews.WEEK) {
formatedDate = momentDate.format(calendarWeekColumnHeader); formatedDate = momentDate.format(calendarWeekColumnHeader);
} else if (view === calendarViews.FORECAST) { } else if (view === calendarViews.FORECAST) {
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates); formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
} }
return ( return (
@@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component {
this.setState({ isOpen: !this.state.isOpen }); this.setState({ isOpen: !this.state.isOpen });
}; };
onSelect = (value) => { onSelect = (newValue) => {
if (Array.isArray(this.props.value)) { const { name, value, values, onChange } = this.props;
let newValue = null; const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
const index = this.props.value.indexOf(value);
if (Array.isArray(value)) {
let arrayValue = null;
const index = value.indexOf(newValue);
if (index === -1) { if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v)); arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
} else { } else {
newValue = [...this.props.value]; arrayValue = [...value];
newValue.splice(index, 1); arrayValue.splice(index, 1);
} }
this.props.onChange({ onChange({
name: this.props.name, name,
value: newValue value: arrayValue,
additionalProperties
}); });
} else { } else {
this.setState({ isOpen: false }); this.setState({ isOpen: false });
this.props.onChange({ onChange({
name: this.props.name, name,
value value: newValue,
additionalProperties
}); });
} }
}; };
@@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component {
values.map((v, index) => { values.map((v, index) => {
const hasParent = v.parentKey !== undefined; const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0; const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey); const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
return ( return (
<OptionComponent <OptionComponent
key={v.key} key={v.key}
@@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput';
const importantFieldNames = [ const importantFieldNames = [
'baseUrl', 'baseUrl',
'apiPath', 'apiPath',
'apiKey' 'apiKey',
'authToken'
]; ];
function getProviderDataKey(providerData) { function getProviderDataKey(providerData) {
@@ -34,7 +35,9 @@ function getSelectOptions(items) {
key: option.value, key: option.value,
value: option.name, value: option.name,
hint: option.hint, hint: option.hint,
parentKey: option.parentValue parentKey: option.parentValue,
isDisabled: option.isDisabled,
additionalProperties: option.additionalProperties
}; };
}); });
} }
@@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = {
provider: PropTypes.string.isRequired, provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired, providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptionsProviderAction: PropTypes.string, selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
@@ -1,5 +0,0 @@
.input {
composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}
-7
View File
@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'input': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,7 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import TextInput from './TextInput'; import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input // Prevent a user from copying (or cutting) the password from the input
function onCopy(e) { function onCopy(e) {
@@ -13,17 +11,14 @@ function PasswordInput(props) {
return ( return (
<TextInput <TextInput
{...props} {...props}
type="password"
onCopy={onCopy} onCopy={onCopy}
/> />
); );
} }
PasswordInput.propTypes = { PasswordInput.propTypes = {
className: PropTypes.string.isRequired ...TextInput.props
};
PasswordInput.defaultProps = {
className: styles.input
}; };
export default PasswordInput; export default PasswordInput;
@@ -21,6 +21,7 @@ function createCleanSeriesSelector() {
tvdbId, tvdbId,
tvMazeId, tvMazeId,
imdbId, imdbId,
tmdbId,
tags = [] tags = []
} = series; } = series;
@@ -33,6 +34,7 @@ function createCleanSeriesSelector() {
tvdbId, tvdbId,
tvMazeId, tvMazeId,
imdbId, imdbId,
tmdbId,
firstCharacter: title.charAt(0).toLowerCase(), firstCharacter: title.charAt(0).toLowerCase(),
tags: tags.reduce((acc, id) => { tags: tags.reduce((acc, id) => {
const matchingTag = allTags.find((tag) => tag.id === id); const matchingTag = allTags.find((tag) => tag.id === id);
@@ -14,6 +14,7 @@ function SeriesSearchResult(props) {
tvdbId, tvdbId,
tvMazeId, tvMazeId,
imdbId, imdbId,
tmdbId,
tags tags
} = props; } = props;
@@ -73,6 +74,14 @@ function SeriesSearchResult(props) {
null null
} }
{
match.key === 'tmdbId' && tmdbId ?
<div className={styles.alternateTitle}>
TmdbId: {tmdbId}
</div> :
null
}
{ {
tag ? tag ?
<div className={styles.tagContainer}> <div className={styles.tagContainer}>
@@ -97,6 +106,7 @@ SeriesSearchResult.propTypes = {
tvdbId: PropTypes.number, tvdbId: PropTypes.number,
tvMazeId: PropTypes.number, tvMazeId: PropTypes.number,
imdbId: PropTypes.string, imdbId: PropTypes.string,
tmdbId: PropTypes.number,
tags: PropTypes.arrayOf(PropTypes.object).isRequired, tags: PropTypes.arrayOf(PropTypes.object).isRequired,
match: PropTypes.object.isRequired match: PropTypes.object.isRequired
}; };
@@ -13,6 +13,7 @@ const fuseOptions = {
'tvdbId', 'tvdbId',
'tvMazeId', 'tvMazeId',
'imdbId', 'imdbId',
'tmdbId',
'tags.label' 'tags.label'
] ]
}; };
+2 -2
View File
@@ -244,7 +244,7 @@ class SignalRConnector extends Component {
handleWantedCutoff = (body) => { handleWantedCutoff = (body) => {
if (body.action === 'updated') { if (body.action === 'updated') {
this.props.dispatchUpdateItem({ this.props.dispatchUpdateItem({
section: 'cutoffUnmet', section: 'wanted.cutoffUnmet',
updateOnly: true, updateOnly: true,
...body.resource ...body.resource
}); });
@@ -254,7 +254,7 @@ class SignalRConnector extends Component {
handleWantedMissing = (body) => { handleWantedMissing = (body) => {
if (body.action === 'updated') { if (body.action === 'updated') {
this.props.dispatchUpdateItem({ this.props.dispatchUpdateItem({
section: 'missing', section: 'wanted.missing',
updateOnly: true, updateOnly: true,
...body.resource ...body.resource
}); });
@@ -15,6 +15,7 @@ class RelativeDateCell extends PureComponent {
className, className,
date, date,
includeSeconds, includeSeconds,
includeTime,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
@@ -39,7 +40,7 @@ class RelativeDateCell extends PureComponent {
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })} title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps} {...otherProps}
> >
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })} {getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })}
</Component> </Component>
); );
} }
@@ -49,6 +50,7 @@ RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
date: PropTypes.string, date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired, includeSeconds: PropTypes.bool.isRequired,
includeTime: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
@@ -60,6 +62,7 @@ RelativeDateCell.propTypes = {
RelativeDateCell.defaultProps = { RelativeDateCell.defaultProps = {
className: styles.cell, className: styles.cell,
includeSeconds: false, includeSeconds: false,
includeTime: false,
component: TableRowCell component: TableRowCell
}; };
@@ -49,11 +49,12 @@ class TableOptionsModal extends Component {
onPageSizeChange = ({ value }) => { onPageSizeChange = ({ value }) => {
let pageSizeError = null; let pageSizeError = null;
const maxPageSize = this.props.maxPageSize ?? 250;
if (value < 5) { if (value < 5) {
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' }); pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
} else if (value > 250) { } else if (value > maxPageSize) {
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' }); pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` });
} else { } else {
this.props.onTableOptionChange({ pageSize: value }); this.props.onTableOptionChange({ pageSize: value });
} }
@@ -248,6 +249,7 @@ TableOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
pageSize: PropTypes.number, pageSize: PropTypes.number,
maxPageSize: PropTypes.number,
canModifyColumns: PropTypes.bool.isRequired, canModifyColumns: PropTypes.bool.isRequired,
optionsComponent: PropTypes.elementType, optionsComponent: PropTypes.elementType,
onTableOptionChange: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired,
-11
View File
@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono'; font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype'); src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
} }
/*
* text-security-disc
*/
@font-face {
font-weight: normal;
font-style: normal;
font-family: 'text-security-disc';
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
}
Binary file not shown.
Binary file not shown.
@@ -111,6 +111,8 @@ class EpisodeHistoryRow extends Component {
<RelativeDateCellConnector <RelativeDateCellConnector
date={date} date={date}
includeSeconds={true}
includeTime={true}
/> />
<TableRowCell className={styles.actions}> <TableRowCell className={styles.actions}>
@@ -14,4 +14,9 @@
.deleteFilesMessage { .deleteFilesMessage {
margin-top: 20px; margin-top: 20px;
color: var(--dangerColor); color: var(--dangerColor);
.deleteCount {
margin-top: 20px;
color: var(--warningColor);
}
} }
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'deleteCount': string;
'deleteFilesMessage': string; 'deleteFilesMessage': string;
'folderPath': string; 'folderPath': string;
'pathContainer': string; 'pathContainer': string;
@@ -50,15 +50,15 @@ class DeleteSeriesModalContent extends Component {
const { const {
title, title,
path, path,
statistics, statistics = {},
deleteOptions, deleteOptions,
onModalClose, onModalClose,
onDeleteOptionChange onDeleteOptionChange
} = this.props; } = this.props;
const { const {
episodeFileCount, episodeFileCount = 0,
sizeOnDisk sizeOnDisk = 0
} = statistics; } = statistics;
const deleteFiles = this.state.deleteFiles; const deleteFiles = this.state.deleteFiles;
@@ -108,16 +108,20 @@ class DeleteSeriesModalContent extends Component {
</FormGroup> </FormGroup>
{ {
deleteFiles && deleteFiles ?
<div className={styles.deleteFilesMessage}> <div className={styles.deleteFilesMessage}>
<div><InlineMarkdown data={translate('DeleteSeriesFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div> <div><InlineMarkdown data={translate('DeleteSeriesFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
{
!!episodeFileCount &&
<div>{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}</div>
}
</div>
}
{
episodeFileCount ?
<div className={styles.deleteCount}>
{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}
</div> :
null
}
</div> :
null
}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
@@ -175,6 +175,7 @@ class SeriesDetails extends Component {
tvdbId, tvdbId,
tvMazeId, tvMazeId,
imdbId, imdbId,
tmdbId,
title, title,
runtime, runtime,
ratings, ratings,
@@ -566,6 +567,7 @@ class SeriesDetails extends Component {
tvdbId={tvdbId} tvdbId={tvdbId}
tvMazeId={tvMazeId} tvMazeId={tvMazeId}
imdbId={imdbId} imdbId={imdbId}
tmdbId={tmdbId}
/> />
} }
kind={kinds.INVERSE} kind={kinds.INVERSE}
@@ -719,6 +721,7 @@ SeriesDetails.propTypes = {
tvdbId: PropTypes.number.isRequired, tvdbId: PropTypes.number.isRequired,
tvMazeId: PropTypes.number, tvMazeId: PropTypes.number,
imdbId: PropTypes.string, imdbId: PropTypes.string,
tmdbId: PropTypes.number,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
@@ -9,7 +9,8 @@ function SeriesDetailsLinks(props) {
const { const {
tvdbId, tvdbId,
tvMazeId, tvMazeId,
imdbId imdbId,
tmdbId
} = props; } = props;
return ( return (
@@ -71,6 +72,22 @@ function SeriesDetailsLinks(props) {
</Label> </Label>
</Link> </Link>
} }
{
!!tmdbId &&
<Link
className={styles.link}
to={`https://www.themoviedb.org/tv/${tmdbId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TMDB
</Label>
</Link>
}
</div> </div>
); );
} }
@@ -78,7 +95,8 @@ function SeriesDetailsLinks(props) {
SeriesDetailsLinks.propTypes = { SeriesDetailsLinks.propTypes = {
tvdbId: PropTypes.number.isRequired, tvdbId: PropTypes.number.isRequired,
tvMazeId: PropTypes.number, tvMazeId: PropTypes.number,
imdbId: PropTypes.string imdbId: PropTypes.string,
tmdbId: PropTypes.number
}; };
export default SeriesDetailsLinks; export default SeriesDetailsLinks;
@@ -14,7 +14,7 @@ function SeriesHistoryModal(props) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE} size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<SeriesHistoryModalContentConnector <SeriesHistoryModalContentConnector
@@ -135,6 +135,8 @@ class SeriesHistoryRow extends Component {
<RelativeDateCellConnector <RelativeDateCellConnector
date={date} date={date}
includeSeconds={true}
includeTime={true}
/> />
<TableRowCell className={styles.actions}> <TableRowCell className={styles.actions}>
@@ -138,7 +138,10 @@ function getInfoRowProps(
}), }),
iconName: icons.CALENDAR, iconName: icons.CALENDAR,
label: label:
getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, { getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat, timeFormat,
timeForToday: true, timeForToday: true,
}) ?? '', }) ?? '',
@@ -156,7 +159,10 @@ function getInfoRowProps(
}), }),
iconName: icons.ADD, iconName: icons.ADD,
label: label:
getRelativeDate(added, shortDateFormat, showRelativeDates, { getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
timeFormat, timeFormat,
timeForToday: true, timeForToday: true,
}) ?? '', }) ?? '',
@@ -232,15 +238,13 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
<SeriesIndexOverviewInfoRow <SeriesIndexOverviewInfoRow
title={formatDateTime(nextAiring, longDateFormat, timeFormat)} title={formatDateTime(nextAiring, longDateFormat, timeFormat)}
iconName={icons.SCHEDULED} iconName={icons.SCHEDULED}
label={getRelativeDate( label={getRelativeDate({
nextAiring, date: nextAiring,
shortDateFormat, shortDateFormat,
showRelativeDates, showRelativeDates,
{ timeFormat,
timeFormat, timeForToday: true,
timeForToday: true, })}
}
)}
/> />
)} )}
@@ -217,7 +217,10 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
timeFormat timeFormat
)}`} )}`}
> >
{getRelativeDate(nextAiring, shortDateFormat, showRelativeDates, { {getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
timeFormat, timeFormat,
timeForToday: true, timeForToday: true,
})} })}
@@ -80,7 +80,10 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
timeFormat timeFormat
)}`} )}`}
> >
{getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, { {getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat, timeFormat,
timeForToday: true, timeForToday: true,
})} })}
@@ -89,15 +92,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
} }
if (sortKey === 'added' && added) { if (sortKey === 'added' && added) {
const addedDate = getRelativeDate( const addedDate = getRelativeDate({
added, date: added,
shortDateFormat, shortDateFormat,
showRelativeDates, showRelativeDates,
{ timeFormat,
timeFormat, timeForToday: false,
timeForToday: false, });
}
);
return ( return (
<div <div
@@ -10,4 +10,15 @@
.path { .path {
margin-left: 5px; margin-left: 5px;
color: var(--dangerColor); color: var(--dangerColor);
font-weight: bold;
}
.statistics {
margin-left: 5px;
color: var(--warningColor);
}
.deleteFilesMessage {
margin-top: 20px;
color: var(--warningColor);
} }
@@ -1,9 +1,11 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'deleteFilesMessage': string;
'message': string; 'message': string;
'path': string; 'path': string;
'pathContainer': string; 'pathContainer': string;
'statistics': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -16,6 +16,7 @@ import Series from 'Series/Series';
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions'; import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import { CheckInputChanged } from 'typings/inputs'; import { CheckInputChanged } from 'typings/inputs';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './DeleteSeriesModalContent.css'; import styles from './DeleteSeriesModalContent.css';
@@ -85,6 +86,24 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
onModalClose, onModalClose,
]); ]);
const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => {
return series.reduce(
(acc, s) => {
const { statistics = { episodeFileCount: 0, sizeOnDisk: 0 } } = s;
const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics;
acc.totalEpisodeFileCount += episodeFileCount;
acc.totalSizeOnDisk += sizeOnDisk;
return acc;
},
{
totalEpisodeFileCount: 0,
totalSizeOnDisk: 0,
}
);
}, [series]);
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('DeleteSelectedSeries')}</ModalHeader> <ModalHeader>{translate('DeleteSelectedSeries')}</ModalHeader>
@@ -137,19 +156,43 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
<ul> <ul>
{series.map((s) => { {series.map((s) => {
const { episodeFileCount = 0, sizeOnDisk = 0 } = s.statistics;
return ( return (
<li key={s.title}> <li key={s.title}>
<span>{s.title}</span> <span>{s.title}</span>
{deleteFiles && ( {deleteFiles && (
<span className={styles.pathContainer}> <span>
-<span className={styles.path}>{s.path}</span> <span className={styles.pathContainer}>
-<span className={styles.path}>{s.path}</span>
</span>
{!!episodeFileCount && (
<span className={styles.statistics}>
(
{translate('DeleteSeriesFolderEpisodeCount', {
episodeFileCount,
size: formatBytes(sizeOnDisk),
})}
)
</span>
)}
</span> </span>
)} )}
</li> </li>
); );
})} })}
</ul> </ul>
{deleteFiles && !!totalEpisodeFileCount ? (
<div className={styles.deleteFilesMessage}>
{translate('DeleteSeriesFolderEpisodeCount', {
episodeFileCount: totalEpisodeFileCount,
size: formatBytes(totalSizeOnDisk),
})}
</div>
) : null}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+1
View File
@@ -70,6 +70,7 @@ interface Series extends ModelBase {
tvdbId: number; tvdbId: number;
tvMazeId: number; tvMazeId: number;
tvRageId: number; tvRageId: number;
tmdbId: number;
useSceneNumbering: boolean; useSceneNumbering: boolean;
year: number; year: number;
isSaving?: boolean; isSaving?: boolean;
@@ -99,6 +99,7 @@ const seriesTokens = [
const seriesIdTokens = [ const seriesIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' }, { token: '{ImdbId}', example: 'tt12345' },
{ token: '{TvdbId}', example: '12345' }, { token: '{TvdbId}', example: '12345' },
{ token: '{TmdbId}', example: '11223' },
{ token: '{TvMazeId}', example: '54321' } { token: '{TvMazeId}', example: '54321' }
]; ];
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { import {
saveNotification, saveNotification,
setNotificationFieldValue, setNotificationFieldValues,
setNotificationValue, setNotificationValue,
testNotification, testNotification,
toggleAdvancedSettings toggleAdvancedSettings
@@ -27,7 +27,7 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
setNotificationValue, setNotificationValue,
setNotificationFieldValue, setNotificationFieldValues,
saveNotification, saveNotification,
testNotification, testNotification,
toggleAdvancedSettings toggleAdvancedSettings
@@ -51,8 +51,8 @@ class EditNotificationModalContentConnector extends Component {
this.props.setNotificationValue({ name, value }); this.props.setNotificationValue({ name, value });
}; };
onFieldChange = ({ name, value }) => { onFieldChange = ({ name, value, additionalProperties = {} }) => {
this.props.setNotificationFieldValue({ name, value }); this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } });
}; };
onSavePress = () => { onSavePress = () => {
@@ -91,7 +91,7 @@ EditNotificationModalContentConnector.propTypes = {
saveError: PropTypes.object, saveError: PropTypes.object,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
setNotificationValue: PropTypes.func.isRequired, setNotificationValue: PropTypes.func.isRequired,
setNotificationFieldValue: PropTypes.func.isRequired, setNotificationFieldValues: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired, toggleAdvancedSettings: PropTypes.func.isRequired,
@@ -0,0 +1,25 @@
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
function createSetProviderFieldValuesReducer(section) {
return (state, { payload }) => {
if (section === payload.section) {
const { properties } = payload;
const newState = getSectionState(state, section);
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
const fields = Object.assign({}, newState.pendingChanges.fields || {});
Object.keys(properties).forEach((name) => {
fields[name] = properties[name];
});
newState.pendingChanges.fields = fields;
return updateSectionState(state, section, newState);
}
return state;
};
}
export default createSetProviderFieldValuesReducer;
@@ -5,6 +5,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetProviderFieldValuesReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema'; import selectProviderSchema from 'Utilities/State/selectProviderSchema';
@@ -22,6 +23,7 @@ export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificati
export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema'; export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema';
export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue'; export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue';
export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue'; export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue';
export const SET_NOTIFICATION_FIELD_VALUES = 'settings/notifications/setNotificationFieldValues';
export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification'; export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification';
export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification'; export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification';
export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification'; export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification';
@@ -55,6 +57,13 @@ export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VAL
}; };
}); });
export const setNotificationFieldValues = createAction(SET_NOTIFICATION_FIELD_VALUES, (payload) => {
return {
section,
...payload
};
});
// //
// Details // Details
@@ -99,6 +108,7 @@ export default {
reducers: { reducers: {
[SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section), [SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section), [SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUES]: createSetProviderFieldValuesReducer(section),
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => { [SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => { return selectProviderSchema(state, section, payload, (selectedSchema) => {
-1
View File
@@ -2,7 +2,6 @@ module.exports = {
// Families // Families
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
passwordFamily: 'text-security-disc',
// Sizes // Sizes
extraSmallFontSize: '11px', extraSmallFontSize: '11px',
@@ -1,43 +0,0 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
if (!date) {
return null;
}
const isTodayDate = isToday(date);
if (isTodayDate && timeForToday && timeFormat) {
return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return translate('Yesterday');
}
if (isTodayDate) {
return translate('Today');
}
if (isTomorrow(date)) {
return translate('Tomorrow');
}
if (isInNextWeek(date)) {
return moment(date).format('dddd');
}
return moment(date).format(shortDateFormat);
}
export default getRelativeDate;
@@ -0,0 +1,82 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
import formatDateTime from './formatDateTime';
interface GetRelativeDateOptions {
date?: string;
shortDateFormat: string;
showRelativeDates: boolean;
timeFormat?: string;
includeSeconds?: boolean;
timeForToday?: boolean;
includeTime?: boolean;
}
function getRelativeDate({
date,
shortDateFormat,
showRelativeDates,
timeFormat,
includeSeconds = false,
timeForToday = false,
includeTime = false,
}: GetRelativeDateOptions) {
if (!date) {
return null;
}
if ((includeTime || timeForToday) && !timeFormat) {
throw new Error(
"getRelativeDate: 'timeFormat' is required when 'includeTime' or 'timeForToday' is true"
);
}
const isTodayDate = isToday(date);
const time = timeFormat
? formatTime(date, timeFormat, {
includeMinuteZero: true,
includeSeconds,
})
: '';
if (isTodayDate && timeForToday) {
return time;
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return includeTime
? translate('YesterdayAt', { time })
: translate('Yesterday');
}
if (isTodayDate) {
return includeTime ? translate('TodayAt', { time }) : translate('Today');
}
if (isTomorrow(date)) {
return includeTime
? translate('TomorrowAt', { time })
: translate('Tomorrow');
}
if (isInNextWeek(date)) {
const day = moment(date).format('dddd');
return includeTime ? translate('DayOfWeekAt', { day, time }) : day;
}
return includeTime
? formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds })
: moment(date).format(shortDateFormat);
}
export default getRelativeDate;
+3 -2
View File
@@ -116,6 +116,7 @@
border: 1px solid var(--inputBorderColor); border: 1px solid var(--inputBorderColor);
border-radius: 4px; border-radius: 4px;
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor); box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
color: var(--textColor);
} }
.form-input:focus { .form-input:focus {
@@ -296,7 +297,7 @@
var light = { var light = {
white: '#fff', white: '#fff',
pageBackground: '#f5f7fa', pageBackground: '#f5f7fa',
textColor: '#656565', textColor: '#515253',
themeDarkColor: '#3a3f51', themeDarkColor: '#3a3f51',
panelBackground: '#fff', panelBackground: '#fff',
inputBackgroundColor: '#fff', inputBackgroundColor: '#fff',
@@ -316,7 +317,7 @@
var dark = { var dark = {
white: '#fff', white: '#fff',
pageBackground: '#202020', pageBackground: '#202020',
textColor: '#656565', textColor: '#ccc',
themeDarkColor: '#494949', themeDarkColor: '#494949',
panelBackground: '#111', panelBackground: '#111',
inputBackgroundColor: '#333', inputBackgroundColor: '#333',
+5 -1
View File
@@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk
{ {
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs); Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.EnumerateDirectories(path); return Directory.EnumerateDirectories(path, "*", new EnumerationOptions
{
AttributesToSkip = FileAttributes.System,
IgnoreInaccessible = true
});
} }
public IEnumerable<string> GetFiles(string path, bool recursive) public IEnumerable<string> GetFiles(string path, bool recursive)
@@ -366,7 +366,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
Mocker.GetMock<IEventAggregator>() Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never()); .Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never());
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending); _trackedDownload.State.Should().Be(TrackedDownloadState.ImportBlocked);
} }
private void AssertImported() private void AssertImported()
@@ -608,5 +608,47 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2); allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2);
allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3); allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3);
} }
[Test]
public async Task episode_search_should_include_series_title_when_not_a_direct_title_match()
{
_xemSeries.Title = "Sonarr's Title";
_xemSeries.CleanTitle = "sonarrstitle";
WithEpisode(1, 12, 2, 3);
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.FindByTvdbId(It.IsAny<int>()))
.Returns(new List<SceneMapping>
{
new SceneMapping
{
TvdbId = _xemSeries.TvdbId,
SearchTerm = "Sonarrs Title",
ParseTerm = _xemSeries.CleanTitle,
SeasonNumber = 1,
SceneSeasonNumber = 1,
SceneOrigin = "tvdb",
Type = "ServicesProvider"
}
});
var allCriteria = WatchForSearchCriteria();
await Subject.EpisodeSearch(_xemEpisodes.First(), false, false);
Mocker.GetMock<ISceneMappingService>()
.Verify(v => v.FindByTvdbId(_xemSeries.Id), Times.Once());
allCriteria.Should().HaveCount(2);
allCriteria.First().Should().BeOfType<SingleEpisodeSearchCriteria>();
allCriteria.First().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(1);
allCriteria.First().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(12);
allCriteria.Last().Should().BeOfType<SingleEpisodeSearchCriteria>();
allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2);
allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3);
}
} }
} }
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_singleEpisodeSearchCriteria = new SingleEpisodeSearchCriteria _singleEpisodeSearchCriteria = new SingleEpisodeSearchCriteria
{ {
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string> { "Monkey Island" }, SceneTitles = new List<string> { "Monkey Island" },
SeasonNumber = 1, SeasonNumber = 1,
EpisodeNumber = 2 EpisodeNumber = 2
@@ -44,14 +44,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_seasonSearchCriteria = new SeasonSearchCriteria _seasonSearchCriteria = new SeasonSearchCriteria
{ {
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string> { "Monkey Island" }, SceneTitles = new List<string> { "Monkey Island" },
SeasonNumber = 1, SeasonNumber = 1,
}; };
_animeSearchCriteria = new AnimeEpisodeSearchCriteria() _animeSearchCriteria = new AnimeEpisodeSearchCriteria()
{ {
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string>() { "Monkey+Island" }, SceneTitles = new List<string>() { "Monkey+Island" },
AbsoluteEpisodeNumber = 100, AbsoluteEpisodeNumber = 100,
SeasonNumber = 5, SeasonNumber = 5,
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_animeSeasonSearchCriteria = new AnimeSeasonSearchCriteria() _animeSeasonSearchCriteria = new AnimeSeasonSearchCriteria()
{ {
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" }, Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string> { "Monkey Island" }, SceneTitles = new List<string> { "Monkey Island" },
SeasonNumber = 3, SeasonNumber = 3,
}; };
@@ -268,6 +268,19 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
page.Url.Query.Should().Contain("imdbid=t40"); page.Url.Query.Should().Contain("imdbid=t40");
} }
[Test]
public void should_search_by_tmdb_if_supported()
{
_capabilities.SupportedTvSearchParameters = new[] { "q", "tmdbid", "season", "ep" };
var results = Subject.GetSearchRequests(_singleEpisodeSearchCriteria);
results.GetTier(0).Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("tmdbid=50");
}
[Test] [Test]
public void should_prefer_search_by_tvdbid_if_rid_supported() public void should_prefer_search_by_tvdbid_if_rid_supported()
{ {
@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
[TestCase(10, "", "", "", null, HdrFormat.None)] [TestCase(10, "", "", "", null, HdrFormat.None)]
[TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)] [TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)]
[TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)] [TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)]
[TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.Hlg10)] [TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.None)]
[TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)] [TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)] [TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)] [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)]
@@ -5,7 +5,6 @@ using FluentValidation.Results;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Test.NotificationTests
[TestFixture] [TestFixture]
public class NotificationBaseFixture : TestBase public class NotificationBaseFixture : TestBase
{ {
private class TestSetting : IProviderConfig private class TestSetting : NotificationSettingsBase<TestSetting>
{ {
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(); return new NzbDroneValidationResult();
} }
@@ -56,5 +56,14 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Subject.GetSeriesFolder(_series) Subject.GetSeriesFolder(_series)
.Should().Be($"Series Title ({_series.TvMazeId})"); .Should().Be($"Series Title ({_series.TvMazeId})");
} }
[Test]
public void should_add_tmdb_id()
{
_namingConfig.SeriesFolderFormat = "{Series Title} ({TmdbId})";
Subject.GetSeriesFolder(_series)
.Should().Be($"Series Title ({_series.TmdbId})");
}
} }
} }
@@ -39,6 +39,13 @@ namespace NzbDrone.Core.Test.ParserTests
ExceptionVerification.IgnoreWarns(); ExceptionVerification.IgnoreWarns();
} }
[TestCase("علم نف) أ.دعادل الأبيض ٢٠٢٤ ٣ ٣")]
[TestCase("ror-240618_1007-1022-")]
public void should_parse_unknown_formats_without_error(string title)
{
Parser.Parser.ParseTitle(title).Should().NotBeNull();
}
[Test] [Test]
public void should_not_parse_md5() public void should_not_parse_md5()
{ {
@@ -33,6 +33,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("2019_08_20_1080_all.mp4", "", 2019, 8, 20)] [TestCase("2019_08_20_1080_all.mp4", "", 2019, 8, 20)]
[TestCase("Series and Title 20201013 Ep7432 [720p WebRip (x264)] [SUBS]", "Series and Title", 2020, 10, 13)] [TestCase("Series and Title 20201013 Ep7432 [720p WebRip (x264)] [SUBS]", "Series and Title", 2020, 10, 13)]
[TestCase("Series Title (1955) - 1954-01-23 05 00 00 - Cottage for Sale.ts", "Series Title (1955)", 1954, 1, 23)] [TestCase("Series Title (1955) - 1954-01-23 05 00 00 - Cottage for Sale.ts", "Series Title (1955)", 1954, 1, 23)]
[TestCase("Series Title - 30-04-2024 HDTV 1080p H264 AAC", "Series Title", 2024, 4, 30)]
// [TestCase("", "", 0, 0, 0)] // [TestCase("", "", 0, 0, 0)]
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day) public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
@@ -100,5 +101,13 @@ namespace NzbDrone.Core.Test.ParserTests
Parser.Parser.ParseTitle(title).Should().BeNull(); Parser.Parser.ParseTitle(title).Should().BeNull();
} }
[TestCase("Tmc - Quotidien - 05-06-2024 HDTV 1080p H264 AAC")]
// [TestCase("", "", 0, 0, 0)]
public void should_not_parse_ambiguous_daily_episode(string postTitle)
{
Parser.Parser.ParseTitle(postTitle).Should().BeNull();
}
} }
} }
@@ -109,6 +109,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Series.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)] [TestCase("The.Series.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)]
[TestCase("The Series (BD)(640x480(RAW) (BATCH 1) (1-13)", false)] [TestCase("The Series (BD)(640x480(RAW) (BATCH 1) (1-13)", false)]
[TestCase("[Doki] Series - 02 (848x480 XviD BD MP3) [95360783]", false)] [TestCase("[Doki] Series - 02 (848x480 XviD BD MP3) [95360783]", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01.BluRay.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray480p_quality(string title, bool proper) public void should_parse_bluray480p_quality(string title, bool proper)
{ {
ParseAndVerifyQuality(title, Quality.Bluray480p, proper); ParseAndVerifyQuality(title, Quality.Bluray480p, proper);
@@ -309,6 +311,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Sans.Series.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)] [TestCase("Sans.Series.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)]
[TestCase("Series.Black.1x01.Selezione.Naturale.ITA.720p.BDMux.x264-NovaRip", false)] [TestCase("Series.Black.1x01.Selezione.Naturale.ITA.720p.BDMux.x264-NovaRip", false)]
[TestCase("Series.Hunter.S02.720p.Blu-ray.Remux.AVC.FLAC.2.0-SiCFoI", false)] [TestCase("Series.Hunter.S02.720p.Blu-ray.Remux.AVC.FLAC.2.0-SiCFoI", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.720p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray720p_quality(string title, bool proper) public void should_parse_bluray720p_quality(string title, bool proper)
{ {
ParseAndVerifyQuality(title, Quality.Bluray720p, proper); ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
@@ -340,6 +343,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S03E01.The.Calm.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR", false)] [TestCase("Series.Title.S03E01.The.Calm.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR", false)]
[TestCase("Series Title Season 2 (BDRemux 1080p HEVC FLAC) [Netaro]", false)] [TestCase("Series Title Season 2 (BDRemux 1080p HEVC FLAC) [Netaro]", false)]
[TestCase("[Vodes] Series Title - Other Title (2020) [BDRemux 1080p HEVC Dual-Audio]", false)] [TestCase("[Vodes] Series Title - Other Title (2020) [BDRemux 1080p HEVC Dual-Audio]", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.1080p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray1080p_remux_quality(string title, bool proper) public void should_parse_bluray1080p_remux_quality(string title, bool proper)
{ {
ParseAndVerifyQuality(title, Quality.Bluray1080pRemux, proper); ParseAndVerifyQuality(title, Quality.Bluray1080pRemux, proper);
@@ -360,6 +364,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S01E08.The.Sonarr.BluRay.2160p.AVC.DTS-HD.MA.5.1.REMUX-FraMeSToR", false)] [TestCase("Series.Title.S01E08.The.Sonarr.BluRay.2160p.AVC.DTS-HD.MA.5.1.REMUX-FraMeSToR", false)]
[TestCase("Series.Title.2x11.Nato.Per.The.Sonarr.Bluray.Remux.AVC.2160p.AC3.ITA", false)] [TestCase("Series.Title.2x11.Nato.Per.The.Sonarr.Bluray.Remux.AVC.2160p.AC3.ITA", false)]
[TestCase("[Dolby Vision] Sonarr.of.Series.S07.MULTi.UHD.BLURAY.REMUX.DV-NoTag", false)] [TestCase("[Dolby Vision] Sonarr.of.Series.S07.MULTi.UHD.BLURAY.REMUX.DV-NoTag", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.2160p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray2160p_remux_quality(string title, bool proper) public void should_parse_bluray2160p_remux_quality(string title, bool proper)
{ {
ParseAndVerifyQuality(title, Quality.Bluray2160pRemux, proper); ParseAndVerifyQuality(title, Quality.Bluray2160pRemux, proper);
@@ -33,6 +33,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)] [TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)]
[TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)] [TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)]
[TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)] [TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)]
[TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)]
[TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)]
public void should_parse_full_season_release(string postTitle, string title, int season) public void should_parse_full_season_release(string postTitle, string title, int season)
{ {
var result = Parser.Parser.ParseTitle(postTitle); var result = Parser.Parser.ParseTitle(postTitle);
@@ -75,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Series.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series 2016", 2, 1)] [TestCase("The.Series.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series 2016", 2, 1)]
[TestCase("The.Series.S07.Vol.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series", 7, 1)] [TestCase("The.Series.S07.Vol.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series", 7, 1)]
[TestCase("The.Series.S06.P1.1080p.Blu-Ray.10-Bit.Dual-Audio.TrueHD.x265-iAHD", "The Series", 6, 1)]
public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart) public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart)
{ {
var result = Parser.Parser.ParseTitle(postTitle); var result = Parser.Parser.ParseTitle(postTitle);
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[www.test-hyphen.ca] - Series (2011) S01", "Series (2011)")] [TestCase("[www.test-hyphen.ca] - Series (2011) S01", "Series (2011)")]
[TestCase("test123.ca - Series Time S02 720p HDTV x264 CRON", "Series Time")] [TestCase("test123.ca - Series Time S02 720p HDTV x264 CRON", "Series Time")]
[TestCase("[www.test-hyphen123.co.za] - Series Title S01E01", "Series Title")] [TestCase("[www.test-hyphen123.co.za] - Series Title S01E01", "Series Title")]
[TestCase("(seriesawake.com) Series Super - 57 [720p] [English Subbed]", "Series Super")]
public void should_not_parse_url_in_name(string postTitle, string title) public void should_not_parse_url_in_name(string postTitle, string title)
{ {
@@ -137,6 +137,20 @@ namespace NzbDrone.Core.Test.TvTests
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TvMazeId == newSeriesInfo.TvMazeId), It.IsAny<bool>(), It.IsAny<bool>())); .Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TvMazeId == newSeriesInfo.TvMazeId), It.IsAny<bool>(), It.IsAny<bool>()));
} }
[Test]
public void should_update_tmdb_id_if_changed()
{
var newSeriesInfo = _series.JsonClone();
newSeriesInfo.TmdbId = _series.TmdbId + 1;
GivenNewSeriesInfo(newSeriesInfo);
Subject.Execute(new RefreshSeriesCommand(new List<int> { _series.Id }));
Mocker.GetMock<ISeriesService>()
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TmdbId == newSeriesInfo.TmdbId), It.IsAny<bool>(), It.IsAny<bool>()));
}
[Test] [Test]
public void should_log_error_if_tvdb_id_not_found() public void should_log_error_if_tvdb_id_not_found()
{ {
@@ -48,11 +48,11 @@ namespace NzbDrone.Core.Test.UpdateTests
{ {
const string branch = "main"; const string branch = "main";
UseRealHttp(); UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(3, 0), null); var recent = Subject.GetRecentUpdates(branch, new Version(4, 0), null);
recent.Should().NotBeEmpty(); recent.Should().NotBeEmpty();
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace()); recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.3.")); recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.4."));
recent.Should().OnlyContain(c => c.ReleaseDate.Year >= 2014); recent.Should().OnlyContain(c => c.ReleaseDate.Year >= 2014);
recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.New != null); recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.New != null);
recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.Fixed != null); recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.Fixed != null);
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace NzbDrone.Core.Annotations namespace NzbDrone.Core.Annotations
@@ -59,13 +60,27 @@ namespace NzbDrone.Core.Annotations
public string Value { get; set; } public string Value { get; set; }
} }
public class FieldSelectOption public class FieldSelectOption<T>
where T : struct
{ {
public int Value { get; set; } public T Value { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int Order { get; set; } public int Order { get; set; }
public string Hint { get; set; } public string Hint { get; set; }
public int? ParentValue { get; set; } public T? ParentValue { get; set; }
public bool? IsDisabled { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
}
public class FieldSelectStringOption
{
public string Value { get; set; }
public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
public string ParentValue { get; set; }
public bool? IsDisabled { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
} }
public enum FieldType public enum FieldType
@@ -242,7 +242,7 @@ namespace NzbDrone.Core.Configuration
{ {
get get
{ {
var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/'); var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/');
if (urlBase.IsNullOrWhiteSpace()) if (urlBase.IsNullOrWhiteSpace())
{ {
@@ -419,13 +419,21 @@ namespace NzbDrone.Core.Configuration
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it."); throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it.");
} }
return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList();
if (config.Count != 1)
{
throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Sonarr will recreate it.");
}
return xDoc;
} }
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME)); newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
return xDoc; return newXDoc;
} }
} }
catch (XmlException ex) catch (XmlException ex)
@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(206)]
public class add_tmdbid : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Series").AddColumn("TmdbId").AsInt32().WithDefaultValue(0);
Create.Index().OnTable("Series").OnColumn("TmdbId");
}
}
}
@@ -25,6 +25,11 @@ namespace NzbDrone.Core.Download.Aggregation
public RemoteEpisode Augment(RemoteEpisode remoteEpisode) public RemoteEpisode Augment(RemoteEpisode remoteEpisode)
{ {
if (remoteEpisode == null)
{
return null;
}
foreach (var augmenter in _augmenters) foreach (var augmenter in _augmenters)
{ {
try try
@@ -1,6 +1,5 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Aria2 namespace NzbDrone.Core.Download.Clients.Aria2
@@ -13,9 +12,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2
} }
} }
public class Aria2Settings : IProviderConfig public class Aria2Settings : DownloadClientSettingsBase<Aria2Settings>
{ {
private static readonly Aria2SettingsValidator Validator = new Aria2SettingsValidator(); private static readonly Aria2SettingsValidator Validator = new ();
public Aria2Settings() public Aria2Settings()
{ {
@@ -44,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")] [FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")]
public string Directory { get; set; } public string Directory { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -2,7 +2,6 @@ using System.ComponentModel;
using FluentValidation; using FluentValidation;
using Newtonsoft.Json; using Newtonsoft.Json;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@@ -18,7 +17,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
} }
} }
public class TorrentBlackholeSettings : IProviderConfig public class TorrentBlackholeSettings : DownloadClientSettingsBase<TorrentBlackholeSettings>
{ {
public TorrentBlackholeSettings() public TorrentBlackholeSettings()
{ {
@@ -26,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
ReadOnly = true; ReadOnly = true;
} }
private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator(); private static readonly TorrentBlackholeSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] [FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")]
[FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")] [FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")]
@@ -48,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
[FieldDefinition(4, Label = "TorrentBlackholeSaveMagnetFilesReadOnly", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText")] [FieldDefinition(4, Label = "TorrentBlackholeSaveMagnetFilesReadOnly", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText")]
public bool ReadOnly { get; set; } public bool ReadOnly { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,6 +1,5 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
} }
} }
public class UsenetBlackholeSettings : IProviderConfig public class UsenetBlackholeSettings : DownloadClientSettingsBase<UsenetBlackholeSettings>
{ {
private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); private static readonly UsenetBlackholeSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")] [FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")]
[FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")] [FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")]
@@ -26,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
[FieldDefinition(1, Label = "BlackholeWatchFolder", Type = FieldType.Path, HelpText = "BlackholeWatchFolderHelpText")] [FieldDefinition(1, Label = "BlackholeWatchFolder", Type = FieldType.Path, HelpText = "BlackholeWatchFolderHelpText")]
public string WatchFolder { get; set; } public string WatchFolder { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -124,14 +124,23 @@ namespace NzbDrone.Core.Download.Clients.Deluge
} }
var items = new List<DownloadClientItem>(); var items = new List<DownloadClientItem>();
var ignoredCount = 0;
foreach (var torrent in torrents) foreach (var torrent in torrents)
{ {
if (torrent.Hash == null) // Silently ignore torrents with no hash
if (torrent.Hash.IsNullOrWhiteSpace())
{ {
continue; continue;
} }
// Ignore torrents without a name, but track to log a single warning for all invalid torrents.
if (torrent.Name.IsNullOrWhiteSpace())
{
ignoredCount++;
continue;
}
var item = new DownloadClientItem(); var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper(); item.DownloadId = torrent.Hash.ToUpper();
item.Title = torrent.Name; item.Title = torrent.Name;
@@ -189,6 +198,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
items.Add(item); items.Add(item);
} }
if (ignoredCount > 0)
{
_logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents");
}
return items; return items;
} }
@@ -1,6 +1,5 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Deluge namespace NzbDrone.Core.Download.Clients.Deluge
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
} }
} }
public class DelugeSettings : IProviderConfig public class DelugeSettings : DownloadClientSettingsBase<DelugeSettings>
{ {
private static readonly DelugeSettingsValidator Validator = new DelugeSettingsValidator(); private static readonly DelugeSettingsValidator Validator = new ();
public DelugeSettings() public DelugeSettings()
{ {
@@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")] [FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")]
public string CompletedDirectory { get; set; } public string CompletedDirectory { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -0,0 +1,30 @@
using System;
using Equ;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients
{
public abstract class DownloadClientSettingsBase<TSettings> : IProviderConfig, IEquatable<TSettings>
where TSettings : DownloadClientSettingsBase<TSettings>
{
private static readonly MemberwiseEqualityComparer<TSettings> Comparer = MemberwiseEqualityComparer<TSettings>.ByProperties;
public abstract NzbDroneValidationResult Validate();
public bool Equals(TSettings other)
{
return Comparer.Equals(this as TSettings, other);
}
public override bool Equals(object obj)
{
return Equals(obj as TSettings);
}
public override int GetHashCode()
{
return Comparer.GetHashCode(this as TSettings);
}
}
}
@@ -1,8 +1,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation namespace NzbDrone.Core.Download.Clients.DownloadStation
@@ -26,9 +25,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
} }
} }
public class DownloadStationSettings : IProviderConfig public class DownloadStationSettings : DownloadClientSettingsBase<DownloadStationSettings>
{ {
private static readonly DownloadStationSettingsValidator Validator = new DownloadStationSettingsValidator(); private static readonly DownloadStationSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; } public string Host { get; set; }
@@ -58,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
this.Port = 5000; this.Port = 5000;
} }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -3,7 +3,6 @@ using System.Linq;
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Download.Clients.Flood.Models; using NzbDrone.Core.Download.Clients.Flood.Models;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Flood namespace NzbDrone.Core.Download.Clients.Flood
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Flood
} }
} }
public class FloodSettings : IProviderConfig public class FloodSettings : DownloadClientSettingsBase<FloodSettings>
{ {
private static readonly FloodSettingsValidator Validator = new FloodSettingsValidator(); private static readonly FloodSettingsValidator Validator = new ();
public FloodSettings() public FloodSettings()
{ {
@@ -69,7 +68,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
[FieldDefinition(10, Label = "DownloadClientFloodSettingsStartOnAdd", Type = FieldType.Checkbox)] [FieldDefinition(10, Label = "DownloadClientFloodSettingsStartOnAdd", Type = FieldType.Checkbox)]
public bool StartOnAdd { get; set; } public bool StartOnAdd { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -2,7 +2,6 @@ using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@@ -34,9 +33,9 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
} }
} }
public class FreeboxDownloadSettings : IProviderConfig public class FreeboxDownloadSettings : DownloadClientSettingsBase<FreeboxDownloadSettings>
{ {
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator(); private static readonly FreeboxDownloadSettingsValidator Validator = new ();
public FreeboxDownloadSettings() public FreeboxDownloadSettings()
{ {
@@ -84,7 +83,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
[FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; } public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,7 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Hadouken namespace NzbDrone.Core.Download.Clients.Hadouken
@@ -22,9 +21,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
} }
} }
public class HadoukenSettings : IProviderConfig public class HadoukenSettings : DownloadClientSettingsBase<HadoukenSettings>
{ {
private static readonly HadoukenSettingsValidator Validator = new HadoukenSettingsValidator(); private static readonly HadoukenSettingsValidator Validator = new ();
public HadoukenSettings() public HadoukenSettings()
{ {
@@ -57,7 +56,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")] [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")]
public string Category { get; set; } public string Category { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,7 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.NzbVortex namespace NzbDrone.Core.Download.Clients.NzbVortex
@@ -23,9 +22,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
} }
} }
public class NzbVortexSettings : IProviderConfig public class NzbVortexSettings : DownloadClientSettingsBase<NzbVortexSettings>
{ {
private static readonly NzbVortexSettingsValidator Validator = new NzbVortexSettingsValidator(); private static readonly NzbVortexSettingsValidator Validator = new ();
public NzbVortexSettings() public NzbVortexSettings()
{ {
@@ -59,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
[FieldDefinition(6, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] [FieldDefinition(6, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")]
public int OlderTvPriority { get; set; } public int OlderTvPriority { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,7 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Nzbget namespace NzbDrone.Core.Download.Clients.Nzbget
@@ -21,9 +20,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
} }
} }
public class NzbgetSettings : IProviderConfig public class NzbgetSettings : DownloadClientSettingsBase<NzbgetSettings>
{ {
private static readonly NzbgetSettingsValidator Validator = new NzbgetSettingsValidator(); private static readonly NzbgetSettingsValidator Validator = new ();
public NzbgetSettings() public NzbgetSettings()
{ {
@@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")] [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")]
public bool AddPaused { get; set; } public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,6 +1,5 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
} }
} }
public class PneumaticSettings : IProviderConfig public class PneumaticSettings : DownloadClientSettingsBase<PneumaticSettings>
{ {
private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator(); private static readonly PneumaticSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")] [FieldDefinition(0, Label = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")]
public string NzbFolder { get; set; } public string NzbFolder { get; set; }
@@ -25,7 +24,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
[FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")] [FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")]
public string StrmFolder { get; set; } public string StrmFolder { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,7 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.QBittorrent namespace NzbDrone.Core.Download.Clients.QBittorrent
@@ -19,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
} }
public class QBittorrentSettings : IProviderConfig public class QBittorrentSettings : DownloadClientSettingsBase<QBittorrentSettings>
{ {
private static readonly QBittorrentSettingsValidator Validator = new QBittorrentSettingsValidator(); private static readonly QBittorrentSettingsValidator Validator = new ();
public QBittorrentSettings() public QBittorrentSettings()
{ {
@@ -74,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] [FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; } public int ContentLayout { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,7 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Sabnzbd namespace NzbDrone.Core.Download.Clients.Sabnzbd
@@ -32,9 +31,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
} }
} }
public class SabnzbdSettings : IProviderConfig public class SabnzbdSettings : DownloadClientSettingsBase<SabnzbdSettings>
{ {
private static readonly SabnzbdSettingsValidator Validator = new SabnzbdSettingsValidator(); private static readonly SabnzbdSettingsValidator Validator = new ();
public SabnzbdSettings() public SabnzbdSettings()
{ {
@@ -78,7 +77,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")] [FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")]
public int OlderTvPriority { get; set; } public int OlderTvPriority { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,10 +1,10 @@
using System; using System;
namespace NzbDrone.Core.Download.Clients namespace NzbDrone.Core.Download.Clients
{ {
public class TorrentSeedConfiguration public class TorrentSeedConfiguration
{ {
public static TorrentSeedConfiguration DefaultConfiguration = new TorrentSeedConfiguration(); public static TorrentSeedConfiguration DefaultConfiguration = new ();
public double? Ratio { get; set; } public double? Ratio { get; set; }
public TimeSpan? SeedTime { get; set; } public TimeSpan? SeedTime { get; set; }
@@ -2,7 +2,6 @@ using System.Text.RegularExpressions;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Transmission namespace NzbDrone.Core.Download.Clients.Transmission
@@ -24,9 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
} }
} }
public class TransmissionSettings : IProviderConfig public class TransmissionSettings : DownloadClientSettingsBase<TransmissionSettings>
{ {
private static readonly TransmissionSettingsValidator Validator = new TransmissionSettingsValidator(); private static readonly TransmissionSettingsValidator Validator = new ();
public TransmissionSettings() public TransmissionSettings()
{ {
@@ -72,7 +71,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] [FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; } public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,6 +1,5 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.RTorrent namespace NzbDrone.Core.Download.Clients.RTorrent
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
} }
} }
public class RTorrentSettings : IProviderConfig public class RTorrentSettings : DownloadClientSettingsBase<RTorrentSettings>
{ {
private static readonly RTorrentSettingsValidator Validator = new RTorrentSettingsValidator(); private static readonly RTorrentSettingsValidator Validator = new ();
public RTorrentSettings() public RTorrentSettings()
{ {
@@ -70,7 +69,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(11, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")] [FieldDefinition(11, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")]
public bool AddStopped { get; set; } public bool AddStopped { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,7 +1,6 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.UTorrent namespace NzbDrone.Core.Download.Clients.UTorrent
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
} }
} }
public class UTorrentSettings : IProviderConfig public class UTorrentSettings : DownloadClientSettingsBase<UTorrentSettings>
{ {
private static readonly UTorrentSettingsValidator Validator = new UTorrentSettingsValidator(); private static readonly UTorrentSettingsValidator Validator = new ();
public UTorrentSettings() public UTorrentSettings()
{ {
@@ -65,7 +64,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")] [FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")]
public int IntialState { get; set; } public int IntialState { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -64,8 +64,8 @@ namespace NzbDrone.Core.Download
SetImportItem(trackedDownload); SetImportItem(trackedDownload);
// Only process tracked downloads that are still downloading // Only process tracked downloads that are still downloading or have been blocked for importing due to an issue with matching
if (trackedDownload.State != TrackedDownloadState.Downloading) if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked)
{ {
return; return;
} }
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Download
if (series == null) if (series == null)
{ {
trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes."); trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes.");
SendManualInteractionRequiredNotification(trackedDownload); SetStateToImportBlocked(trackedDownload);
return; return;
} }
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.Download
if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch) if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch)
{ {
trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details."); trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details.");
SendManualInteractionRequiredNotification(trackedDownload); SetStateToImportBlocked(trackedDownload);
return; return;
} }
@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download
if (trackedDownload.RemoteEpisode == null) if (trackedDownload.RemoteEpisode == null)
{ {
trackedDownload.Warn("Unable to parse download, automatic import is not possible."); trackedDownload.Warn("Unable to parse download, automatic import is not possible.");
SendManualInteractionRequiredNotification(trackedDownload); SetStateToImportBlocked(trackedDownload);
return; return;
} }
@@ -187,7 +187,7 @@ namespace NzbDrone.Core.Download
if (statusMessages.Any()) if (statusMessages.Any())
{ {
trackedDownload.Warn(statusMessages.ToArray()); trackedDownload.Warn(statusMessages.ToArray());
SendManualInteractionRequiredNotification(trackedDownload); SetStateToImportBlocked(trackedDownload);
} }
} }
@@ -254,8 +254,10 @@ namespace NzbDrone.Core.Download
return false; return false;
} }
private void SendManualInteractionRequiredNotification(TrackedDownload trackedDownload) private void SetStateToImportBlocked(TrackedDownload trackedDownload)
{ {
trackedDownload.State = TrackedDownloadState.ImportBlocked;
if (!trackedDownload.HasNotifiedManualInteractionRequired) if (!trackedDownload.HasNotifiedManualInteractionRequired)
{ {
var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList(); var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList();
@@ -1,14 +1,35 @@
using NzbDrone.Core.Indexers; using System;
using Equ;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
{ {
public class DownloadClientDefinition : ProviderDefinition public class DownloadClientDefinition : ProviderDefinition, IEquatable<DownloadClientDefinition>
{ {
private static readonly MemberwiseEqualityComparer<DownloadClientDefinition> Comparer = MemberwiseEqualityComparer<DownloadClientDefinition>.ByProperties;
[MemberwiseEqualityIgnore]
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } = 1; public int Priority { get; set; } = 1;
public bool RemoveCompletedDownloads { get; set; } = true; public bool RemoveCompletedDownloads { get; set; } = true;
public bool RemoveFailedDownloads { get; set; } = true; public bool RemoveFailedDownloads { get; set; } = true;
public bool Equals(DownloadClientDefinition other)
{
return Comparer.Equals(this, other);
}
public override bool Equals(object obj)
{
return Equals(obj as DownloadClientDefinition);
}
public override int GetHashCode()
{
return Comparer.GetHashCode(this);
}
} }
} }
@@ -73,8 +73,8 @@ namespace NzbDrone.Core.Download
public void Check(TrackedDownload trackedDownload) public void Check(TrackedDownload trackedDownload)
{ {
// Only process tracked downloads that are still downloading // Only process tracked downloads that are still downloading or import is blocked (if they fail after attempting to be processed)
if (trackedDownload.State != TrackedDownloadState.Downloading) if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked)
{ {
return; return;
} }
@@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
_trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition,
downloadItem); downloadItem);
if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading) if (trackedDownload is { State: TrackedDownloadState.Downloading or TrackedDownloadState.ImportBlocked })
{ {
_failedDownloadService.Check(trackedDownload); _failedDownloadService.Check(trackedDownload);
_completedDownloadService.Check(trackedDownload); _completedDownloadService.Check(trackedDownload);
@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
public enum TrackedDownloadState public enum TrackedDownloadState
{ {
Downloading, Downloading,
ImportBlocked,
ImportPending, ImportPending,
Importing, Importing,
Imported, Imported,
@@ -28,6 +28,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
public class TrackedDownloadService : ITrackedDownloadService, public class TrackedDownloadService : ITrackedDownloadService,
IHandle<EpisodeInfoRefreshedEvent>, IHandle<EpisodeInfoRefreshedEvent>,
IHandle<SeriesAddedEvent>,
IHandle<SeriesDeletedEvent> IHandle<SeriesDeletedEvent>
{ {
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
@@ -278,12 +279,29 @@ namespace NzbDrone.Core.Download.TrackedDownloads
} }
} }
public void Handle(SeriesAddedEvent message)
{
var cachedItems = _cache.Values
.Where(t =>
t.RemoteEpisode?.Series == null ||
message.Series?.TvdbId == t.RemoteEpisode.Series.TvdbId)
.ToList();
if (cachedItems.Any())
{
cachedItems.ForEach(UpdateCachedItem);
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads()));
}
}
public void Handle(SeriesDeletedEvent message) public void Handle(SeriesDeletedEvent message)
{ {
var cachedItems = _cache.Values.Where(t => var cachedItems = _cache.Values
t.RemoteEpisode?.Series != null && .Where(t =>
message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id)) t.RemoteEpisode?.Series != null &&
.ToList(); message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id || s.TvdbId == t.RemoteEpisode.Series.TvdbId))
.ToList();
if (cachedItems.Any()) if (cachedItems.Any())
{ {
@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
Tvdb = series.TvdbId.ToString(); Tvdb = series.TvdbId.ToString();
TvMaze = series.TvMazeId > 0 ? series.TvMazeId.ToString() : null; TvMaze = series.TvMazeId > 0 ? series.TvMazeId.ToString() : null;
TvRage = series.TvRageId > 0 ? series.TvMazeId.ToString() : null; TvRage = series.TvRageId > 0 ? series.TvMazeId.ToString() : null;
Tmdb = series.TmdbId > 0 ? series.TmdbId.ToString() : null;
Imdb = series.ImdbId; Imdb = series.ImdbId;
} }
} }
@@ -29,18 +29,17 @@ namespace NzbDrone.Core.ImportLists.AniList
} }
} }
public class AniListSettingsBase<TSettings> : IImportListSettings public class AniListSettingsBase<TSettings> : ImportListSettingsBase<TSettings>
where TSettings : AniListSettingsBase<TSettings> where TSettings : AniListSettingsBase<TSettings>
{ {
protected virtual AbstractValidator<TSettings> Validator => new AniListSettingsBaseValidator<TSettings>(); private static readonly AniListSettingsBaseValidator<TSettings> Validator = new ();
public AniListSettingsBase() public AniListSettingsBase()
{ {
BaseUrl = "https://graphql.anilist.co";
SignIn = "startOAuth"; SignIn = "startOAuth";
} }
public string BaseUrl { get; set; } public override string BaseUrl { get; set; } = "https://graphql.anilist.co";
[FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessToken { get; set; } public string AccessToken { get; set; }
@@ -54,7 +53,7 @@ namespace NzbDrone.Core.ImportLists.AniList
[FieldDefinition(99, Label = "ImportListsAniListSettingsAuthenticateWithAniList", Type = FieldType.OAuth)] [FieldDefinition(99, Label = "ImportListsAniListSettingsAuthenticateWithAniList", Type = FieldType.OAuth)]
public string SignIn { get; set; } public string SignIn { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); return new NzbDroneValidationResult(Validator.Validate((TSettings)this));
} }
@@ -1,12 +1,12 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.AniList.List namespace NzbDrone.Core.ImportLists.AniList.List
{ {
public class AniListSettingsValidator : AniListSettingsBaseValidator<AniListSettings> public class AniListSettingsValidator : AniListSettingsBaseValidator<AniListSettings>
{ {
public AniListSettingsValidator() public AniListSettingsValidator()
: base()
{ {
RuleFor(c => c.Username).NotEmpty(); RuleFor(c => c.Username).NotEmpty();
@@ -18,10 +18,11 @@ namespace NzbDrone.Core.ImportLists.AniList.List
public class AniListSettings : AniListSettingsBase<AniListSettings> public class AniListSettings : AniListSettingsBase<AniListSettings>
{ {
public const string sectionImport = "Import List Status"; public const string SectionImport = "Import List Status";
private static readonly AniListSettingsValidator Validator = new ();
public AniListSettings() public AniListSettings()
: base()
{ {
ImportCurrent = true; ImportCurrent = true;
ImportPlanning = true; ImportPlanning = true;
@@ -29,42 +30,45 @@ namespace NzbDrone.Core.ImportLists.AniList.List
ImportFinished = true; ImportFinished = true;
} }
protected override AbstractValidator<AniListSettings> Validator => new AniListSettingsValidator();
[FieldDefinition(1, Label = "Username", HelpText = "ImportListsAniListSettingsUsernameHelpText")] [FieldDefinition(1, Label = "Username", HelpText = "ImportListsAniListSettingsUsernameHelpText")]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(2, Label = "ImportListsAniListSettingsImportWatching", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportWatchingHelpText")] [FieldDefinition(2, Label = "ImportListsAniListSettingsImportWatching", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportWatchingHelpText")]
public bool ImportCurrent { get; set; } public bool ImportCurrent { get; set; }
[FieldDefinition(3, Label = "ImportListsAniListSettingsImportPlanning", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportPlanningHelpText")] [FieldDefinition(3, Label = "ImportListsAniListSettingsImportPlanning", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportPlanningHelpText")]
public bool ImportPlanning { get; set; } public bool ImportPlanning { get; set; }
[FieldDefinition(4, Label = "ImportListsAniListSettingsImportCompleted", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportCompletedHelpText")] [FieldDefinition(4, Label = "ImportListsAniListSettingsImportCompleted", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportCompletedHelpText")]
public bool ImportCompleted { get; set; } public bool ImportCompleted { get; set; }
[FieldDefinition(5, Label = "ImportListsAniListSettingsImportDropped", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportDroppedHelpText")] [FieldDefinition(5, Label = "ImportListsAniListSettingsImportDropped", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportDroppedHelpText")]
public bool ImportDropped { get; set; } public bool ImportDropped { get; set; }
[FieldDefinition(6, Label = "ImportListsAniListSettingsImportPaused", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportPausedHelpText")] [FieldDefinition(6, Label = "ImportListsAniListSettingsImportPaused", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportPausedHelpText")]
public bool ImportPaused { get; set; } public bool ImportPaused { get; set; }
[FieldDefinition(7, Label = "ImportListsAniListSettingsImportRepeating", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportRepeatingHelpText")] [FieldDefinition(7, Label = "ImportListsAniListSettingsImportRepeating", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportRepeatingHelpText")]
public bool ImportRepeating { get; set; } public bool ImportRepeating { get; set; }
[FieldDefinition(8, Label = "ImportListsAniListSettingsImportFinished", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportFinishedHelpText")] [FieldDefinition(8, Label = "ImportListsAniListSettingsImportFinished", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportFinishedHelpText")]
public bool ImportFinished { get; set; } public bool ImportFinished { get; set; }
[FieldDefinition(9, Label = "ImportListsAniListSettingsImportReleasing", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportReleasingHelpText")] [FieldDefinition(9, Label = "ImportListsAniListSettingsImportReleasing", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportReleasingHelpText")]
public bool ImportReleasing { get; set; } public bool ImportReleasing { get; set; }
[FieldDefinition(10, Label = "ImportListsAniListSettingsImportNotYetReleased", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportNotYetReleasedHelpText")] [FieldDefinition(10, Label = "ImportListsAniListSettingsImportNotYetReleased", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportNotYetReleasedHelpText")]
public bool ImportUnreleased { get; set; } public bool ImportUnreleased { get; set; }
[FieldDefinition(11, Label = "ImportListsAniListSettingsImportCancelled", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportCancelledHelpText")] [FieldDefinition(11, Label = "ImportListsAniListSettingsImportCancelled", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportCancelledHelpText")]
public bool ImportCancelled { get; set; } public bool ImportCancelled { get; set; }
[FieldDefinition(12, Label = "ImportListsAniListSettingsImportHiatus", Type = FieldType.Checkbox, Section = sectionImport, HelpText = "ImportListsAniListSettingsImportHiatusHelpText")] [FieldDefinition(12, Label = "ImportListsAniListSettingsImportHiatus", Type = FieldType.Checkbox, Section = SectionImport, HelpText = "ImportListsAniListSettingsImportHiatusHelpText")]
public bool ImportHiatus { get; set; } public bool ImportHiatus { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
} }
} }
@@ -13,19 +13,14 @@ namespace NzbDrone.Core.ImportLists.Custom
} }
} }
public class CustomSettings : IImportListSettings public class CustomSettings : ImportListSettingsBase<CustomSettings>
{ {
private static readonly CustomSettingsValidator Validator = new CustomSettingsValidator(); private static readonly CustomSettingsValidator Validator = new ();
public CustomSettings()
{
BaseUrl = "";
}
[FieldDefinition(0, Label = "ImportListsCustomListSettingsUrl", HelpText = "ImportListsCustomListSettingsUrlHelpText")] [FieldDefinition(0, Label = "ImportListsCustomListSettingsUrl", HelpText = "ImportListsCustomListSettingsUrlHelpText")]
public string BaseUrl { get; set; } public override string BaseUrl { get; set; } = string.Empty;
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.ImportLists.Imdb
// Parse TSV response from IMDB export // Parse TSV response from IMDB export
var rows = importResponse.Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); var rows = importResponse.Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
series = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 1).SelectList(i => new ImportListItemInfo { ImdbId = i[1] }); series = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 5).SelectList(i => new ImportListItemInfo { ImdbId = i[1], Title = i[5] });
return series; return series;
} }
@@ -14,16 +14,16 @@ namespace NzbDrone.Core.ImportLists.Imdb
} }
} }
public class ImdbListSettings : IImportListSettings public class ImdbListSettings : ImportListSettingsBase<ImdbListSettings>
{ {
private static readonly ImdbSettingsValidator Validator = new ImdbSettingsValidator(); private static readonly ImdbSettingsValidator Validator = new ();
public string BaseUrl { get; set; } public override string BaseUrl { get; set; }
[FieldDefinition(1, Label = "ImportListsImdbSettingsListId", HelpText = "ImportListsImdbSettingsListIdHelpText")] [FieldDefinition(1, Label = "ImportListsImdbSettingsListId", HelpText = "ImportListsImdbSettingsListIdHelpText")]
public string ListId { get; set; } public string ListId { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -1,11 +1,14 @@
using System; using System;
using Equ;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.ImportLists namespace NzbDrone.Core.ImportLists
{ {
public class ImportListDefinition : ProviderDefinition public class ImportListDefinition : ProviderDefinition, IEquatable<ImportListDefinition>
{ {
private static readonly MemberwiseEqualityComparer<ImportListDefinition> Comparer = MemberwiseEqualityComparer<ImportListDefinition>.ByProperties;
public bool EnableAutomaticAdd { get; set; } public bool EnableAutomaticAdd { get; set; }
public bool SearchForMissingEpisodes { get; set; } public bool SearchForMissingEpisodes { get; set; }
public MonitorTypes ShouldMonitor { get; set; } public MonitorTypes ShouldMonitor { get; set; }
@@ -15,10 +18,31 @@ namespace NzbDrone.Core.ImportLists
public bool SeasonFolder { get; set; } public bool SeasonFolder { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
[MemberwiseEqualityIgnore]
public override bool Enable => EnableAutomaticAdd; public override bool Enable => EnableAutomaticAdd;
[MemberwiseEqualityIgnore]
public ImportListStatus Status { get; set; } public ImportListStatus Status { get; set; }
[MemberwiseEqualityIgnore]
public ImportListType ListType { get; set; } public ImportListType ListType { get; set; }
[MemberwiseEqualityIgnore]
public TimeSpan MinRefreshInterval { get; set; } public TimeSpan MinRefreshInterval { get; set; }
public bool Equals(ImportListDefinition other)
{
return Comparer.Equals(this, other);
}
public override bool Equals(object obj)
{
return Equals(obj as ImportListDefinition);
}
public override int GetHashCode()
{
return Comparer.GetHashCode(this);
}
} }
} }
@@ -0,0 +1,31 @@
using System;
using Equ;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists
{
public abstract class ImportListSettingsBase<TSettings> : IImportListSettings, IEquatable<TSettings>
where TSettings : ImportListSettingsBase<TSettings>
{
private static readonly MemberwiseEqualityComparer<TSettings> Comparer = MemberwiseEqualityComparer<TSettings>.ByProperties;
public abstract string BaseUrl { get; set; }
public abstract NzbDroneValidationResult Validate();
public bool Equals(TSettings other)
{
return Comparer.Equals(this as TSettings, other);
}
public override bool Equals(object obj)
{
return Equals(obj as TSettings);
}
public override int GetHashCode()
{
return Comparer.GetHashCode(this as TSettings);
}
}
}
@@ -24,16 +24,11 @@ namespace NzbDrone.Core.ImportLists.MyAnimeList
} }
} }
public class MyAnimeListSettings : IImportListSettings public class MyAnimeListSettings : ImportListSettingsBase<MyAnimeListSettings>
{ {
public string BaseUrl { get; set; } private static readonly MalSettingsValidator Validator = new ();
protected AbstractValidator<MyAnimeListSettings> Validator => new MalSettingsValidator(); public override string BaseUrl { get; set; } = "https://api.myanimelist.net/v2";
public MyAnimeListSettings()
{
BaseUrl = "https://api.myanimelist.net/v2";
}
[FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")] [FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")]
public int ListStatus { get; set; } public int ListStatus { get; set; }
@@ -50,7 +45,7 @@ namespace NzbDrone.Core.ImportLists.MyAnimeList
[FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)] [FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)]
public string SignIn { get; set; } public string SignIn { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -14,9 +14,9 @@ namespace NzbDrone.Core.ImportLists.Plex
} }
} }
public class PlexListSettings : IImportListSettings public class PlexListSettings : ImportListSettingsBase<PlexListSettings>
{ {
protected virtual PlexListSettingsValidator Validator => new PlexListSettingsValidator(); private static readonly PlexListSettingsValidator Validator = new ();
public PlexListSettings() public PlexListSettings()
{ {
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.ImportLists.Plex
public virtual string Scope => ""; public virtual string Scope => "";
public string BaseUrl { get; set; } public override string BaseUrl { get; set; }
[FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessToken { get; set; } public string AccessToken { get; set; }
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.ImportLists.Plex
[FieldDefinition(99, Label = "ImportListsPlexSettingsAuthenticateWithPlex", Type = FieldType.OAuth)] [FieldDefinition(99, Label = "ImportListsPlexSettingsAuthenticateWithPlex", Type = FieldType.OAuth)]
public string SignIn { get; set; } public string SignIn { get; set; }
public NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
} }
@@ -12,9 +12,9 @@ namespace NzbDrone.Core.ImportLists.Rss.Plex
} }
} }
public class PlexRssImportSettings : RssImportBaseSettings public class PlexRssImportSettings : RssImportBaseSettings<PlexRssImportSettings>
{ {
private PlexRssImportSettingsValidator Validator => new (); private static readonly PlexRssImportSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "ImportListsSettingsRssUrl", Type = FieldType.Textbox, HelpLink = "https://app.plex.tv/desktop/#!/settings/watchlist")] [FieldDefinition(0, Label = "ImportListsSettingsRssUrl", Type = FieldType.Textbox, HelpLink = "https://app.plex.tv/desktop/#!/settings/watchlist")]
public override string Url { get; set; } public override string Url { get; set; }
@@ -8,7 +8,7 @@ using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.Rss namespace NzbDrone.Core.ImportLists.Rss
{ {
public class RssImportBase<TSettings> : HttpImportListBase<TSettings> public class RssImportBase<TSettings> : HttpImportListBase<TSettings>
where TSettings : RssImportBaseSettings, new() where TSettings : RssImportBaseSettings<TSettings>, new()
{ {
public override string Name => "RSS List Base"; public override string Name => "RSS List Base";
public override ImportListType ListType => ImportListType.Advanced; public override ImportListType ListType => ImportListType.Advanced;
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.ImportLists.Rss
public override IImportListRequestGenerator GetRequestGenerator() public override IImportListRequestGenerator GetRequestGenerator()
{ {
return new RssImportRequestGenerator return new RssImportRequestGenerator<TSettings>
{ {
Settings = Settings Settings = Settings
}; };

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