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

Compare commits

...

27 Commits

Author SHA1 Message Date
Weblate
4b8afe3d33 Multiple Translations updated by Weblate
ignore-downstream

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

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

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

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

View File

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

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

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

View File

@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4
VERSION: 4.0.2
VERSION: 4.0.3
jobs:
backend:

1
.gitignore vendored
View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -444,6 +444,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Name (2020) - S01E20 - [AAC 2.0].ru-something-else.srt", new string[0], "something-else", "Russian")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].Full Subtitles.eng.ass", new string[0], "Full Subtitles", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle - 1.en.ass", new string[0], "mytitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle 1.en.ass", new string[0], "mytitle 1", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle.en.ass", new string[0], "mytitle", "English")]
public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage)
{

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Extras
{
public interface IExistingExtraFiles
{
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles);
List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename);
}
public class ExistingExtraFileService : IExistingExtraFiles, IHandle<SeriesScannedEvent>
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras
_logger = logger;
}
public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles)
public List<string> ImportExtraFiles(Series series, List<string> possibleExtraFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing extra files in {0}", series.Path);
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Extras
foreach (var existingExtraFileImporter in _existingExtraFileImporters)
{
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles);
var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles, fileNameBeforeRename);
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath)));
}
@@ -45,7 +45,7 @@ namespace NzbDrone.Core.Extras
{
var series = message.Series;
var possibleExtraFiles = message.PossibleExtraFiles;
var importedFiles = ImportExtraFiles(series, possibleExtraFiles);
var importedFiles = ImportExtraFiles(series, possibleExtraFiles, null);
_logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count);
}

View File

@@ -7,6 +7,6 @@ namespace NzbDrone.Core.Extras
public interface IImportExistingExtraFiles
{
int Order { get; }
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename);
}
}

View File

@@ -19,12 +19,21 @@ namespace NzbDrone.Core.Extras
}
public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles);
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename);
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles)
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles, bool keepExistingEntries)
{
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id);
if (keepExistingEntries)
{
var incompleteImports = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance).Select(f => f.Id);
_extraFileService.DeleteMany(incompleteImports);
return Filter(series, filesOnDisk, importedFiles, new List<TExtraFile>());
}
Clean(series, filesOnDisk, importedFiles, seriesFiles);
return Filter(series, filesOnDisk, importedFiles, seriesFiles);

View File

@@ -33,12 +33,12 @@ namespace NzbDrone.Core.Extras.Metadata
public override int Order => 0;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing metadata in {0}", series.Path);
var metadataFiles = new List<MetadataFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
foreach (var possibleMetadataFile in filterResult.FilesOnDisk)
{

View File

@@ -28,12 +28,12 @@ namespace NzbDrone.Core.Extras.Others
public override int Order => 2;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing extra files in {0}", series.Path);
var extraFiles = new List<OtherExtraFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
foreach (var possibleExtraFile in filterResult.FilesOnDisk)
{

View File

@@ -29,12 +29,12 @@ namespace NzbDrone.Core.Extras.Subtitles
public override int Order => 1;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles)
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles, string fileNameBeforeRename)
{
_logger.Debug("Looking for existing subtitle files in {0}", series.Path);
var subtitleFiles = new List<SubtitleFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles);
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null);
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
{
@@ -46,7 +46,8 @@ namespace NzbDrone.Core.Extras.Subtitles
{
FileEpisodeInfo = Parser.Parser.ParsePath(possibleSubtitleFile),
Series = series,
Path = possibleSubtitleFile
Path = possibleSubtitleFile,
FileNameBeforeRename = fileNameBeforeRename
};
try

View File

@@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using NzbDrone.Core.AutoTagging;
using NzbDrone.Core.AutoTagging.Specifications;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers
@@ -9,17 +11,24 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public class CleanupUnusedTags : IHousekeepingTask
{
private readonly IMainDatabase _database;
private readonly IAutoTaggingRepository _autoTaggingRepository;
public CleanupUnusedTags(IMainDatabase database)
public CleanupUnusedTags(IMainDatabase database, IAutoTaggingRepository autoTaggingRepository)
{
_database = database;
_autoTaggingRepository = autoTaggingRepository;
}
public void Clean()
{
using var mapper = _database.OpenConnection();
var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" }
var usedTags = new[]
{
"Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers",
"AutoTagging", "DownloadClients"
}
.SelectMany(v => GetUsedTags(v, mapper))
.Concat(GetAutoTaggingTagSpecificationTags(mapper))
.Distinct()
.ToArray();
@@ -37,10 +46,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
private int[] GetUsedTags(string table, IDbConnection mapper)
{
return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
return mapper
.Query<List<int>>(
$"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
.SelectMany(x => x)
.Distinct()
.ToArray();
}
private List<int> GetAutoTaggingTagSpecificationTags(IDbConnection mapper)
{
var tags = new List<int>();
var autoTags = _autoTaggingRepository.All();
foreach (var autoTag in autoTags)
{
foreach (var specification in autoTag.Specifications)
{
if (specification is TagSpecification tagSpec)
{
tags.Add(tagSpec.Value);
}
}
}
return tags;
}
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
@@ -12,6 +13,7 @@ using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.AniList.List
{
@@ -153,5 +155,63 @@ namespace NzbDrone.Core.ImportLists.AniList.List
return new ImportListFetchResult(CleanupListItems(releases), anyFailure);
}
protected override ValidationFailure TestConnection()
{
try
{
var parser = GetParser();
var generator = GetRequestGenerator();
var pageIndex = 1;
var continueTesting = true;
var hasResults = false;
// Anilist caps the result list to 50 items at maximum per query, so the data must be pulled in batches.
// The number of pages are not known upfront, so the fetch logic must be changed to look at the returned page data.
do
{
var currentRequest = generator.GetRequest(pageIndex);
var response = FetchImportListResponse(currentRequest);
var page = parser.ParseResponse(response, out var pageInfo).ToList();
// Continue testing additional pages if all results were filtered out by 'Media' filters and there are additional pages
continueTesting = pageInfo.HasNextPage && page.Count == 0;
pageIndex = pageInfo.CurrentPage + 1;
hasResults = page.Count > 0;
}
while (continueTesting);
if (!hasResults)
{
return new NzbDroneValidationFailure(string.Empty,
"No results were returned from your import list, please check your settings and the log for details.")
{ IsWarning = true };
}
}
catch (RequestLimitReachedException)
{
_logger.Warn("Request limit reached");
}
catch (UnsupportedFeedException ex)
{
_logger.Warn(ex, "Import list feed is not supported");
return new ValidationFailure(string.Empty, "Import list feed is not supported: " + ex.Message);
}
catch (ImportListException ex)
{
_logger.Warn(ex, "Unable to connect to import list");
return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details.");
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to import list");
return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details.");
}
return null;
}
}
}

View File

@@ -136,6 +136,7 @@
"AutoTaggingSpecificationRootFolder": "Root Folder",
"AutoTaggingSpecificationSeriesType": "Series Type",
"AutoTaggingSpecificationStatus": "Status",
"AutoTaggingSpecificationTag": "Tag",
"Automatic": "Automatic",
"AutomaticAdd": "Automatic Add",
"AutomaticSearch": "Automatic Search",
@@ -218,6 +219,7 @@
"ClickToChangeLanguage": "Click to change language",
"ClickToChangeQuality": "Click to change quality",
"ClickToChangeReleaseGroup": "Click to change release group",
"ClickToChangeReleaseType": "Click to change release type",
"ClickToChangeSeason": "Click to change season",
"ClickToChangeSeries": "Click to change series",
"ClientPriority": "Client Priority",
@@ -278,6 +280,7 @@
"CustomFormats": "Custom Formats",
"CustomFormatsLoadError": "Unable to load Custom Formats",
"CustomFormatsSettings": "Custom Formats Settings",
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationLanguage": "Language",
@@ -378,7 +381,7 @@
"DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?",
"Deleted": "Deleted",
"DeletedReasonEpisodeMissingFromDisk": "{appName} was unable to find the file on disk so the file was unlinked from the episode in the database",
"DeletedReasonManual": "File was deleted by via UI",
"DeletedReasonManual": "File was deleted using {appName}, either manually or by another tool through the API",
"DeletedReasonUpgrade": "File was deleted to import an upgrade",
"DeletedSeriesDescription": "Series was deleted from TheTVDB",
"Destination": "Destination",
@@ -635,6 +638,7 @@
"EpisodeRequested": "Episode Requested",
"EpisodeSearchResultsLoadError": "Unable to load results for this episode search. Try again later",
"EpisodeTitle": "Episode Title",
"EpisodeTitleFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Episode Title:30}`) or the beginning (e.g. `{Episode Title:-30}`) are both supported. Episode titles will be automatically truncated to file system limitations if necessary.",
"EpisodeTitleRequired": "Episode Title Required",
"EpisodeTitleRequiredHelpText": "Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA",
"Episodes": "Episodes",
@@ -1406,6 +1410,8 @@
"NotificationsTelegramSettingsBotToken": "Bot Token",
"NotificationsTelegramSettingsChatId": "Chat ID",
"NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages",
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications",
"NotificationsTelegramSettingsSendSilently": "Send Silently",
"NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound",
"NotificationsTelegramSettingsTopicId": "Topic ID",
@@ -1589,6 +1595,7 @@
"RelativePath": "Relative Path",
"Release": "Release",
"ReleaseGroup": "Release Group",
"ReleaseGroupFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Release Group:30}`) or the beginning (e.g. `{Release Group:-30}`) are both supported.`).",
"ReleaseGroups": "Release Groups",
"ReleaseHash": "Release Hash",
"ReleaseProfile": "Release Profile",
@@ -1774,11 +1781,13 @@
"SelectLanguages": "Select Languages",
"SelectQuality": "Select Quality",
"SelectReleaseGroup": "Select Release Group",
"SelectReleaseType": "Select Release Type",
"SelectSeason": "Select Season",
"SelectSeasonModalTitle": "{modalTitle} - Select Season",
"SelectSeries": "Select Series",
"SendAnonymousUsageData": "Send Anonymous Usage Data",
"Series": "Series",
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them]({url}) .",
"SeriesCannotBeFound": "Sorry, that series cannot be found.",
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} episode files",

View File

@@ -267,7 +267,7 @@
"DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada",
"DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?",
"DisabledForLocalAddresses": "Deshabilitado para Direcciones Locales",
"DeletedReasonManual": "El archivo fue borrado por vía UI",
"DeletedReasonManual": "El archivo fue eliminado usando {appName}, o bien manualmente o por otra herramienta a través de la API",
"ClearBlocklist": "Limpiar lista de bloqueos",
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña",
"MonitorPilotEpisode": "Episodio Piloto",
@@ -324,7 +324,7 @@
"ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados",
"ConnectSettings": "Conectar Ajustes",
"CustomFormatUnknownCondition": "Condición de Formato Personalizado Desconocida '{implementation}'",
"XmlRpcPath": "Ruta XML RPC",
"XmlRpcPath": "Ruta RPC de XML",
"AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no se aplicará si esta condición {implementationName} coincide.",
"CloneCustomFormat": "Clonar formato personalizado",
"Close": "Cerrar",
@@ -530,7 +530,7 @@
"DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series",
"DoNotPrefer": "No preferir",
"DoNotUpgradeAutomatically": "No actualizar automáticamente",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, en blanco usa el defecto por el cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"Download": "Descargar",
"Donate": "Donar",
"DownloadClientDelugeValidationLabelPluginFailure": "Falló la configuración de la etiqueta",
@@ -543,7 +543,7 @@
"DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene varios archivos y no está en una carpeta de trabajo: {outputPath}",
"DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opción requiere al menos NzbGet versión 16.0",
"DownloadClientOptionsLoadError": "No es posible cargar las opciones del cliente de descarga",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta tendrá que ser accesible desde XBMC",
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC",
"DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb",
"Docker": "Docker",
"DockerUpdater": "Actualiza el contenedor docker para recibir la actualización",
@@ -579,8 +579,8 @@
"DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API Freebox con la versión de la API, p. ej. '{url}', por defecto a '{defaultApiUrl}'",
"DownloadClientFreeboxSettingsAppId": "ID de la app",
"DownloadClientFreeboxSettingsAppTokenHelpText": "Token de la app recuperado cuando se crea acceso a la API de Freebox (esto es 'app_token')",
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, por defecto es '{port}'",
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{url}' (solo funcionará en la misma red)",
"DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, predeterminado a '{port}'",
"DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP de host del Freebox, predeterminado a '{url}' (solo funcionará en la misma red)",
"DownloadClientFreeboxUnableToReachFreeboxApi": "No es posible acceder a la API de Freebox. Verifica la configuración 'URL de la API' para la URL base y la versión.",
"DownloadClientNzbgetValidationKeepHistoryOverMax": "La opción KeepHistory de NZBGet debería ser menor de 25000",
"DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La opción KeepHistory de NzbGet está establecida demasiado alta.",
@@ -605,7 +605,7 @@
"EnableHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato",
"EnableMediaInfoHelpText": "Extraer información de video como la resolución, el tiempo de ejecución y la información del códec de los archivos. Esto requiere que {appName} lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.",
"TheLogLevelDefault": "El nivel de registro por defecto es 'Info' y puede ser cambiado en [Opciones generales](opciones/general)",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, por defecto es '.magnet'",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, predeterminado a '.magnet'",
"DownloadIgnoredEpisodeTooltip": "Descarga de episodio ignorada",
"EditDelayProfile": "Editar perfil de retraso",
"DownloadClientFloodSettingsUrlBaseHelpText": "Añade un prefijo a la API de Flood, como {url}",
@@ -613,13 +613,13 @@
"EditReleaseProfile": "Editar perfil de lanzamiento",
"DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm",
"DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría",
"DownloadClientRTorrentSettingsUrlPath": "Ruta de la url",
"DownloadClientRTorrentSettingsUrlPath": "Ruta de url",
"DownloadClientSabnzbdValidationDevelopVersion": "Versión de desarrollo de Sabnzbd, asumiendo versión 3.0.0 o superior.",
"DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} de rastrear nuevas descargas. Sabnzbd también recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que resulta más efectivo.",
"DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.",
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debe deshabilitar la ordenación por fechas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar carpetas de trabajo",
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url de {clientName}, como {url}",
"DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url {clientName}, como {url}",
"DownloadClientStatusAllClientHealthCheckMessage": "Ningún cliente de descarga está disponible debido a fallos",
"DownloadClientValidationGroupMissing": "El grupo no existe",
"DownloadClientValidationSslConnectFailure": "No es posible conectarse a través de SSL",
@@ -649,7 +649,7 @@
"DownloadClientFloodSettingsStartOnAdd": "Inicial al añadir",
"DownloadClientFreeboxApiError": "La API de Freebox devolvió el error: {errorDescription}",
"DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. El motivo: {errorDescription}",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Se importarán los archivos .strm en esta carpeta por drone",
"DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error",
"DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial",
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado",
@@ -658,26 +658,26 @@
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar descargas completadas sin una categoría.",
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Poner en cola el torrent no está habilitado en los ajustes de su qBittorrent. Habilítelo en qBittorrent o seleccione 'Último' como prioridad.",
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} no podrá efectuar el Manejo de Descargas Completadas según lo configurado. Puede arreglar esto en qBittorrent ('Herramientas -> Opciones...' en el menú) cambiando 'Opciones -> BitTorrent -> Límite de ratio ' de 'Eliminarlas' a 'Pausarlas'",
"DownloadClientRTorrentSettingsAddStopped": "Añadir parados",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitarlo añadirá los torrents y magnets a rTorrent en un estado parado. Esto puede romper los archivos magnet.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de rTorrent",
"DownloadClientRTorrentSettingsAddStopped": "Añadir detenido",
"DownloadClientRTorrentSettingsAddStoppedHelpText": "Permite añadir torrents y magnets a rTorrent en estado detenido. Esto puede romper los archivos magnet.",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de rTorrent",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El cliente de descarga {downloadClientName} se establece para eliminar las descargas completadas. Esto puede resultar en descargas siendo eliminadas de tu cliente antes de que {appName} pueda importarlas.",
"DownloadClientRootFolderHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en la carpeta raíz {rootFolderPath}. No debería descargar a una carpeta raíz.",
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar la ordenación por fechas",
"DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debe deshabilitar la ordenación de películas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.",
"DownloadClientSettingsCategorySubFolderHelpText": "Añadir una categoría específica a {appName} evita conflictos con descargas no-{appName} no relacionadas. Usar una categoría es opcional, pero bastante recomendado. Crea un subdirectorio [categoría] en el directorio de salida.",
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, déjelo en blanco para usar el predeterminado",
"DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado",
"DownloadClientSettingsInitialState": "Estado inicial",
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a {clientName}",
"DownloadClientSettingsInitialStateHelpText": "Estado inicial para torrents añadidos a {clientName}",
"DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión hace más de 14 días",
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está descargando metadatos",
"DownloadClientSettingsPostImportCategoryHelpText": "Categoría para {appName} que se establece después de que se haya importado la descarga. {appName} no eliminará los torrents en esa categoría incluso si finalizó la siembra. Déjelo en blanco para mantener la misma categoría.",
"DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión dentro de los últimos 14 días",
"DownloadClientSettingsUseSslHelpText": "Usa conexión segura cuando haya una conexión a {clientName}",
"DownloadClientSettingsUseSslHelpText": "Usa una conexión segura cuando haya una conexión a {clientName}",
"DownloadClientSortingHealthCheckMessage": "El cliente de descarga {downloadClientName} tiene habilitada la ordenación {sortingMode} para la categoría de {appName}. Debería deshabilitar la ordenación en su cliente de descarga para evitar problemas al importar.",
"DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de descarga no disponibles debido a fallos: {downloadClientNames}",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de Transmission",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p.ej. {url}, por defecto es '{defaultUrl}'",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Transmission",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p. ej. {url}, predeterminado a '{defaultUrl}'",
"DownloadClientUTorrentTorrentStateError": "uTorrent está informando de un error",
"DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta",
"DownloadClientValidationApiKeyRequired": "Clave API requerida",
@@ -706,11 +706,11 @@
"DownloadClientSabnzbdValidationUnknownVersion": "Versión desconocida: {rawVersion}",
"DownloadClientSettingsAddPaused": "Añadir pausado",
"DownloadClientSeriesTagHelpText": "Solo use este cliente de descarga para series con al menos una etiqueta coincidente. Déjelo en blanco para usarlo con todas las series.",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Consulte Opciones -> Interfaz Web -> 'Usar HTTPS en lugar de HTTP' en qBittorrent.",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Ver en Opciones -> Interfaz web -> 'Usar HTTPS en lugar de HTTP' en qbittorrent.",
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} no pudo añadir la etiqueta a qBittorrent.",
"DownloadClientQbittorrentValidationCategoryUnsupported": "La categoría no está soportada",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Poner en cola no está habilitado",
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, vea {url}. Esto es usualmente RPC2 o [ruta a rTorrent]{url2} cuando se usa rTorrent.",
"DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, ver {url}. Esto es usualmente RPC2 o [ruta a ruTorrent]{url2} cuando se usa ruTorrent.",
"DownloadClientQbittorrentTorrentStatePathError": "No es posible importar. La ruta coincide con el directorio de descarga base del cliente, ¿es posible que 'Mantener carpeta de nivel superior' esté deshabilitado para este torrent o que 'Diseño de contenido de torrent' NO se haya establecido en 'Original' o 'Crear subcarpeta'?",
"DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar la ordenación de películas",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenación de TV",
@@ -733,8 +733,8 @@
"DownloadClientValidationSslConnectFailureDetail": "{appName} no se puede conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor, intente configurar tanto {appName} como {clientName} para no usar SSL.",
"DownloadFailedEpisodeTooltip": "La descarga del episodio falló",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primero las primeras y últimas",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Tenga en cuenta que Torrents forzados no se atiene a las restricciones de sembrado",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una Carpeta Compartida con el nombre '{sharedFolder}', ¿estás seguro que lo has especificado correctamente?",
"EnableCompletedDownloadHandlingHelpText": "Importar automáticamente las descargas completas del gestor de descargas",
@@ -1005,8 +1005,8 @@
"Forecast": "Previsión",
"IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador",
"IndexerHDBitsSettingsCodecs": "Códecs",
"IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usan todas las opciones.",
"IndexerPriority": "Prioridad del indexador",
"IconForFinales": "Icono para Finales",
"IgnoreDownload": "Ignorar descarga",
@@ -1068,23 +1068,23 @@
"IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores",
"IndexerSearchNoInteractiveHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Interactiva activada, {appName} no proporcionará ningún resultado de búsquedas interactivas",
"PasswordConfirmation": "Confirmación de Contraseña",
"IndexerSettingsAdditionalParameters": "Parámetros Adicionales",
"IndexerSettingsAdditionalParameters": "Parámetros adicionales",
"IndexerSettingsAllowZeroSizeHelpText": "Activar esta opción le permitirá utilizar fuentes que no especifiquen el tamaño del lanzamiento, pero tenga cuidado, no se realizarán comprobaciones relacionadas con el tamaño.",
"IndexerSettingsAllowZeroSize": "Permitir Tamaño Cero",
"StopSelecting": "Detener la Selección",
"IndexerSettingsCookie": "Cookie",
"IndexerSettingsCategories": "Categorías",
"IndexerSettingsMinimumSeedersHelpText": "Número mínimo de semillas necesario.",
"IndexerSettingsSeedRatio": "Proporción de Semillado",
"IndexerSettingsSeedRatio": "Ratio de sembrado",
"StartupDirectory": "Directorio de Arranque",
"IndexerSettingsAdditionalParametersNyaa": "Parámetros Adicionales",
"IndexerSettingsPasskey": "Clave de acceso",
"IndexerSettingsSeasonPackSeedTime": "Tiempo de Semillado de los Pack de Temporada",
"IndexerSettingsAnimeStandardFormatSearch": "Formato Estándar de Búsqueda de Anime",
"IndexerSettingsAnimeStandardFormatSearchHelpText": "Buscar también anime utilizando la numeración estándar",
"IndexerSettingsApiPathHelpText": "Ruta a la api, normalmente {url}",
"IndexerSettingsApiPathHelpText": "Ruta a la API, usualmente {url}",
"IndexerSettingsSeasonPackSeedTimeHelpText": "La cantidad de tiempo que un torrent de pack de temporada debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
"IndexerSettingsSeedTime": "Tiempo de Semillado",
"IndexerSettingsSeedTime": "Tiempo de sembrado",
"IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores",
"IndexerValidationCloudFlareCaptchaExpired": "El token CAPTCHA de CloudFlare ha caducado, actualícelo.",
"NotificationsDiscordSettingsAuthor": "Autor",
@@ -1097,12 +1097,12 @@
"IndexerSettingsMinimumSeeders": "Semillas mínimas",
"IndexerSettingsRssUrl": "URL de RSS",
"IndexerSettingsAnimeCategoriesHelpText": "Lista desplegable, dejar en blanco para desactivar anime",
"IndexerSettingsApiPath": "Ruta de la API",
"IndexerSettingsApiPath": "Ruta de API",
"IndexerSettingsCookieHelpText": "Si su sitio requiere una cookie de inicio de sesión para acceder al RSS, tendrá que conseguirla a través de un navegador.",
"IndexerSettingsRssUrlHelpText": "Introduzca la URL de un canal RSS compatible con {indexer}",
"IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}",
"IndexerHDBitsSettingsMediums": "Medios",
"IndexerSettingsSeedTimeHelpText": "La cantidad de tiempo que un torrent debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser compartido antes de detenerse, vació usa el predeterminado del cliente de descarga",
"IndexerValidationCloudFlareCaptchaRequired": "Sitio protegido por CloudFlare CAPTCHA. Se requiere un token CAPTCHA válido.",
"NotificationsEmailSettingsUseEncryption": "Usar Cifrado",
"LastDuration": "Última Duración",
@@ -2057,5 +2057,14 @@
"NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).",
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)",
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador."
"ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador.",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar con MyAnimeList",
"ImportListsMyAnimeListSettingsListStatus": "Estado de lista",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista desde la que quieres importar, establecer a 'Todo' para todas las listas",
"CustomFormatsSettingsTriggerInfo": "Un formato personalizado será aplicado al lanzamiento o archivo cuando coincida con al menos uno de los diferentes tipos de condición elegidos.",
"ClickToChangeReleaseType": "Haz clic para cambiar el tipo de lanzamiento",
"ReleaseGroupFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Grupo de lanzamiento:30}`) como desde el principio (p. ej. `{Grupo de lanzamiento:-30}`).",
"SelectReleaseType": "Seleccionar tipo de lanzamiento",
"SeriesFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de serie:30}`) como desde el principio (p. ej. `{Título de serie:-30}`).",
"EpisodeTitleFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de episodio:30}`) como desde el principio (p. ej. `{Título de episodio:-30}`). Los títulos de episodio serán truncados automáticamente acorde a las limitaciones del sistema de archivos si es necesario."
}

View File

@@ -1102,7 +1102,7 @@
"ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Activer la gestion des téléchargements terminés si possible (multi-ordinateur non pris en charge)",
"ImportMechanismHandlingDisabledHealthCheckMessage": "Activer la gestion des téléchargements terminés",
"ImportUsingScript": "Importer à l'aide d'un script",
"IncludeHealthWarnings": "Inclure des avertissements de santé",
"IncludeHealthWarnings": "Inclure les avertissements de santé",
"Indexer": "Indexeur",
"LibraryImportTipsSeriesUseRootFolder": "Pointez {appName} vers le dossier contenant toutes vos émissions de télévision, pas une en particulier. par exemple. \"`{goodFolderExample}`\" et non \"`{badFolderExample}`\". De plus, chaque série doit se trouver dans son propre dossier dans le dossier racine/bibliothèque.",
"Links": "Liens",
@@ -1249,7 +1249,7 @@
"Debug": "Déboguer",
"DelayProfileSeriesTagsHelpText": "S'applique aux séries avec au moins une balise correspondante",
"DelayingDownloadUntil": "Retarder le téléchargement jusqu'au {date} à {time}",
"DeletedReasonManual": "Le fichier a été supprimé via l'interface utilisateur",
"DeletedReasonManual": "Le fichier a été supprimé à l'aide de {appName}, soit manuellement, soit par un autre outil via l'API.",
"DeleteRemotePathMapping": "Supprimer la correspondance de chemin distant",
"DestinationPath": "Chemin de destination",
"DestinationRelativePath": "Chemin relatif de destination",
@@ -2057,5 +2057,14 @@
"NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise",
"DatabaseMigration": "Migration des bases de données",
"Filters": "Filtres",
"ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double."
"ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double.",
"ImportListsMyAnimeListSettingsListStatus": "Statut de la liste",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Type de liste à partir de laquelle vous souhaitez importer, défini sur 'All' pour toutes les listes",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authentifiez-vous avec MyAnimeList",
"CustomFormatsSettingsTriggerInfo": "Un format personnalisé sera appliqué à une version ou à un fichier lorsqu'il correspond à au moins un de chacun des différents types de conditions choisis.",
"ClickToChangeReleaseType": "Cliquez pour changer le type de version",
"EpisodeTitleFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Episode Title:30}`) ou du début (par exemple `{Episode Title:-30}`) sont toutes deux prises en charge. Les titres des épisodes seront automatiquement tronqués en fonction des limitations du système de fichiers si nécessaire.",
"SelectReleaseType": "Sélectionnez le type de version",
"SeriesFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Series Title:30}`) ou du début (par exemple `{Series Title:-30}`) sont toutes deux prises en charge.",
"ReleaseGroupFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Release Group:30}`) ou du début (par exemple `{Release Group:-30}`) sont toutes deux prises en charge.`)."
}

View File

@@ -605,11 +605,11 @@
"Importing": "Importando",
"IncludeCustomFormatWhenRenaming": "Incluir formato personalizado ao renomear",
"IncludeCustomFormatWhenRenamingHelpText": "Incluir no formato de renomeação {Custom Formats}",
"IncludeHealthWarnings": "Incluir Advertências de Saúde",
"IncludeHealthWarnings": "Incluir Alertas de Saúde",
"IndexerDownloadClientHelpText": "Especifique qual cliente de download é usado para baixar deste indexador",
"IndexerOptionsLoadError": "Não foi possível carregar as opções do indexador",
"IndexerPriority": "Prioridade do indexador",
"IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado como desempate para lançamentos iguais ao obter lançamentos, o {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS",
"IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado ao capturar lançamentos como desempate para lançamentos iguais, {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS",
"IndexerSettings": "Configurações do indexador",
"IndexersLoadError": "Não foi possível carregar os indexadores",
"IndexersSettingsSummary": "Indexadores e opções de indexador",
@@ -2057,5 +2057,9 @@
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Local opcional para mover os downloads concluídos, deixe em branco para usar o local padrão do Deluge",
"DownloadClientDelugeSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Deluge",
"EpisodeRequested": "Episódio Pedido",
"ReleaseProfileIndexerHelpTextWarning": "Definir um indexador específico em um perfil de lançamento fará com que esse perfil seja aplicado apenas a lançamentos desse indexador."
"ReleaseProfileIndexerHelpTextWarning": "Definir um indexador específico em um perfil de lançamento fará com que esse perfil seja aplicado apenas a lançamentos desse indexador.",
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar com MyAnimeList",
"ImportListsMyAnimeListSettingsListStatus": "Status da Lista",
"ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista da qual você deseja importar, defina como 'Todas' para todas as listas",
"CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos um de cada um dos diferentes tipos de condição escolhidos."
}

View File

@@ -198,5 +198,7 @@
"AppUpdated": "{appName} actualizat",
"ShowRelativeDatesHelpText": "Afișați datele relative (Azi / Ieri / etc) sau absolute",
"WeekColumnHeader": "Antetul coloanei săptămânii",
"TimeFormat": "Format ora"
"TimeFormat": "Format ora",
"CustomFilter": "Filtru personalizat",
"CustomFilters": "Filtre personalizate"
}

View File

@@ -94,5 +94,15 @@
"AudioLanguages": "Мови аудіо",
"AuthForm": "Форми (сторінка входу)",
"Authentication": "Автентифікація",
"AuthenticationMethod": "Метод автентифікації"
"AuthenticationMethod": "Метод автентифікації",
"Yes": "Так",
"AuthenticationRequired": "Потрібна Автентифікація",
"UpdateAll": "Оновити все",
"WhatsNew": "Що нового ?",
"Yesterday": "Вчора",
"AddedToDownloadQueue": "Додано в чергу на завантаження",
"AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.",
"AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм",
"AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль",
"AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача"
}

View File

@@ -174,10 +174,16 @@ namespace NzbDrone.Core.MediaFiles
fileInfoStopwatch.Stop();
_logger.Trace("Reprocessing existing files complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed);
var filesOnDisk = GetNonVideoFiles(series.Path);
var possibleExtraFiles = FilterPaths(series.Path, filesOnDisk);
RemoveEmptySeriesFolder(series.Path);
var possibleExtraFiles = new List<string>();
if (_diskProvider.FolderExists(series.Path))
{
var extraFiles = GetNonVideoFiles(series.Path);
possibleExtraFiles = FilterPaths(series.Path, extraFiles);
}
CompletedScanning(series, possibleExtraFiles);
}

View File

@@ -123,6 +123,11 @@ namespace NzbDrone.Core.MediaFiles
episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath);
if (localEpisode is not null)
{
localEpisode.FileNameBeforeRename = episodeFile.RelativePath;
}
if (localEpisode is not null && _scriptImportDecider.TryImport(episodeFilePath, destinationFilePath, localEpisode, episodeFile, mode) is var scriptImportDecision && scriptImportDecision != ScriptImportDecision.DeferMove)
{
if (scriptImportDecision == ScriptImportDecision.RenameRequested)
@@ -130,7 +135,6 @@ namespace NzbDrone.Core.MediaFiles
try
{
MoveEpisodeFile(episodeFile, series, episodeFile.Episodes);
localEpisode.FileRenamedAfterScriptImport = true;
}
catch (SameFilenameException)
{

View File

@@ -38,16 +38,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators
var firstEpisode = localEpisode.Episodes.First();
var episodeFile = firstEpisode.EpisodeFile.Value;
localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path);
localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path, localEpisode.FileNameBeforeRename);
return localEpisode;
}
public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path)
public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path, string fileNameBeforeRename)
{
var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path);
var episodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.RelativePath);
var episodeFileTitle = Path.GetFileNameWithoutExtension(fileNameBeforeRename ?? episodeFile.RelativePath);
var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.OriginalFilePath) ?? string.Empty;
if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase)))

View File

@@ -123,6 +123,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
else
{
episodeFile.IndexerFlags = localEpisode.IndexerFlags;
episodeFile.ReleaseType = localEpisode.ReleaseType;
}
// Fall back to parsed information if history is unavailable or missing
@@ -176,9 +177,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
if (localEpisode.ScriptImported)
{
_existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles);
_existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, localEpisode.FileNameBeforeRename);
if (localEpisode.FileRenamedAfterScriptImport)
if (localEpisode.FileNameBeforeRename != episodeFile.RelativePath)
{
_extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile);
}

View File

@@ -129,28 +129,30 @@ namespace NzbDrone.Core.MediaFiles
[EventHandleOrder(EventHandleOrder.Last)]
public void Handle(EpisodeFileDeletedEvent message)
{
if (_configService.DeleteEmptyFolders)
if (!_configService.DeleteEmptyFolders || message.Reason == DeleteMediaFileReason.MissingFromDisk)
{
var series = message.EpisodeFile.Series.Value;
var seriesPath = series.Path;
var folder = message.EpisodeFile.Path.GetParentPath();
return;
}
while (seriesPath.IsParentPath(folder))
var series = message.EpisodeFile.Series.Value;
var seriesPath = series.Path;
var folder = message.EpisodeFile.Path.GetParentPath();
while (seriesPath.IsParentPath(folder))
{
if (_diskProvider.FolderExists(folder))
{
if (_diskProvider.FolderExists(folder))
{
_diskProvider.RemoveEmptySubfolders(folder);
}
folder = folder.GetParentPath();
_diskProvider.RemoveEmptySubfolders(folder);
}
_diskProvider.RemoveEmptySubfolders(seriesPath);
folder = folder.GetParentPath();
}
if (_diskProvider.FolderEmpty(seriesPath))
{
_diskProvider.DeleteFolder(seriesPath, true);
}
_diskProvider.RemoveEmptySubfolders(seriesPath);
if (_diskProvider.FolderEmpty(seriesPath))
{
_diskProvider.DeleteFolder(seriesPath, true);
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
@@ -115,18 +114,18 @@ namespace NzbDrone.Core.Notifications.Ntfy
{
try
{
requestBuilder.Headers.Add("X-Title", title);
requestBuilder.Headers.Add("X-Message", message);
requestBuilder.Headers.Add("X-Priority", settings.Priority.ToString());
requestBuilder.AddQueryParam("title", title);
requestBuilder.AddQueryParam("message", message);
requestBuilder.AddQueryParam("priority", settings.Priority.ToString());
if (settings.Tags.Any())
{
requestBuilder.Headers.Add("X-Tags", settings.Tags.Join(","));
requestBuilder.AddQueryParam("tags", settings.Tags.Join(","));
}
if (!settings.ClickUrl.IsNullOrWhiteSpace())
{
requestBuilder.Headers.Add("X-Click", settings.ClickUrl);
requestBuilder.AddQueryParam("click", settings.ClickUrl);
}
if (!settings.AccessToken.IsNullOrWhiteSpace())

View File

@@ -18,47 +18,65 @@ namespace NzbDrone.Core.Notifications.Telegram
public override void OnGrab(GrabMessage grabMessage)
{
_proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE;
_proxy.SendNotification(title, grabMessage.Message, Settings);
}
public override void OnDownload(DownloadMessage message)
{
_proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE;
_proxy.SendNotification(title, message.Message, Settings);
}
public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage)
{
_proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE;
_proxy.SendNotification(title, deleteMessage.Message, Settings);
}
public override void OnSeriesAdd(SeriesAddMessage message)
{
_proxy.SendNotification(SERIES_ADDED_TITLE, message.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE;
_proxy.SendNotification(title, message.Message, Settings);
}
public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage)
{
_proxy.SendNotification(SERIES_DELETED_TITLE, deleteMessage.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE;
_proxy.SendNotification(title, deleteMessage.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE;
_proxy.SendNotification(title, healthCheck.Message, Settings);
}
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
{
_proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE;
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE;
_proxy.SendNotification(title, updateMessage.Message, Settings);
}
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
{
_proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings);
var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE;
_proxy.SendNotification(title, message.Message, Settings);
}
public override ValidationResult Test()

View File

@@ -54,10 +54,11 @@ namespace NzbDrone.Core.Notifications.Telegram
{
try
{
const string brandedTitle = "Sonarr - Test Notification";
const string title = "Test Notification";
const string body = "This is a test message from Sonarr";
SendNotification(title, body, settings);
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings);
}
catch (Exception ex)
{

View File

@@ -32,6 +32,9 @@ namespace NzbDrone.Core.Notifications.Telegram
[FieldDefinition(3, Label = "NotificationsTelegramSettingsSendSilently", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsSendSilentlyHelpText")]
public bool SendSilently { get; set; }
[FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")]
public bool IncludeAppNameInTitle { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -706,7 +706,7 @@ namespace NzbDrone.Core.Organizer
return string.Empty;
}
return customFormats.Where(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat).FirstOrDefault()?.ToString() ?? string.Empty;
return customFormats.FirstOrDefault(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat)?.ToString() ?? string.Empty;
};
}
@@ -719,7 +719,7 @@ namespace NzbDrone.Core.Organizer
private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter)
{
var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming);
var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming).ToList();
var filteredTokens = tokens;

View File

@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SubtitleLanguageTitleRegex = new Regex(@".+?(\.((?<tags1>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*[-_. ](?<title>[^.]*)(\.((?<tags2>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SubtitleTitleRegex = new Regex(@"((?<title>.+) - )?(?<copy>(?<!\d+)\d{1,3}(?!\d+))$", RegexOptions.Compiled);
private static readonly Regex SubtitleTitleRegex = new Regex(@"^((?<title>.+) - )?(?<copy>(?<!\d+)\d{1,3}(?!\d+))$", RegexOptions.Compiled);
public static List<Language> ParseLanguages(string title)
{

View File

@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Parser.Model
public int CustomFormatScore { get; set; }
public GrabbedReleaseInfo Release { get; set; }
public bool ScriptImported { get; set; }
public bool FileRenamedAfterScriptImport { get; set; }
public string FileNameBeforeRename { get; set; }
public bool ShouldImportExtras { get; set; }
public List<string> PossibleExtraFiles { get; set; }
public SubtitleTitleInfo SubtitleInfo { get; set; }

View File

@@ -24,6 +24,7 @@ namespace NzbDrone.Core.Parser.Model
public bool IsMultiSeason { get; set; }
public bool IsSeasonExtra { get; set; }
public bool IsSplitEpisode { get; set; }
public bool IsMiniSeries { get; set; }
public bool Special { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }

View File

@@ -555,7 +555,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
private static readonly Regex SeasonFolderRegex = new Regex(@"^(?:S|Season|Saison|Series|Stagione)[-_. ]*(?<season>(?<!\d+)\d{1,4}(?!\d+))(?:[_. ]+(?!\d+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ](?!\d+)(?<remaining>.+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled);
@@ -564,31 +564,40 @@ namespace NzbDrone.Core.Parser
public static ParsedEpisodeInfo ParsePath(string path)
{
var fileInfo = new FileInfo(path);
var result = ParseTitle(fileInfo.Name);
// Parse using the folder and file separately, but combine if they both parse correctly.
var episodeNumberMatch = SimpleEpisodeNumberRegex.Matches(fileInfo.Name);
var episodeNumberMatch = SimpleEpisodeNumberRegex.Match(fileInfo.Name);
if (episodeNumberMatch.Count != 0 && fileInfo.Directory?.Name != null)
if (episodeNumberMatch.Success && fileInfo.Directory?.Name != null && (result == null || result.IsMiniSeries || result.AbsoluteEpisodeNumbers.Any()))
{
var parsedFileInfo = ParseMatchCollection(episodeNumberMatch, fileInfo.Name);
var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name);
if (parsedFileInfo != null)
if (seasonMatch.Success && seasonMatch.Groups["season"].Success)
{
var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name);
var episodeCaptures = episodeNumberMatch.Groups["episode"].Captures.Cast<Capture>().ToList();
var first = ParseNumber(episodeCaptures.First().Value);
var last = ParseNumber(episodeCaptures.Last().Value);
var pathTitle = $"S{seasonMatch.Groups["season"].Value}E{first:00}";
if (seasonMatch.Success && seasonMatch.Groups["season"].Success)
if (first != last)
{
parsedFileInfo.SeasonNumber = int.Parse(seasonMatch.Groups["season"].Value);
Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo);
return parsedFileInfo;
pathTitle += $"-E{last:00}";
}
if (episodeNumberMatch.Groups["remaining"].Success)
{
pathTitle += $" {episodeNumberMatch.Groups["remaining"].Value}";
}
var parsedFileInfo = ParseTitle(pathTitle);
Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo);
return parsedFileInfo;
}
}
var result = ParseTitle(fileInfo.Name);
if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number))
{
Logger.Debug("Attempting to parse episode info using directory and file names. {0}", fileInfo.Directory.Name);
@@ -1107,6 +1116,7 @@ namespace NzbDrone.Core.Parser
{
// If no season was found and it's not an absolute only release it should be treated as a mini series and season 1
result.SeasonNumber = 1;
result.IsMiniSeries = true;
}
}
else

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.AutoTagging;
using NzbDrone.Core.AutoTagging.Specifications;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download;
using NzbDrone.Core.ImportLists;
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Tags
var restrictions = _releaseProfileService.All();
var series = _seriesService.GetAllSeriesTags();
var indexers = _indexerService.All();
var autotags = _autoTaggingService.All();
var autoTags = _autoTaggingService.All();
var downloadClients = _downloadClientFactory.All();
var details = new List<TagDetails>();
@@ -137,7 +138,7 @@ namespace NzbDrone.Core.Tags
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
SeriesIds = series.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(),
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
AutoTagIds = GetAutoTagIds(tag, autoTags),
DownloadClientIds = downloadClients.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
});
}
@@ -188,5 +189,23 @@ namespace NzbDrone.Core.Tags
_repo.Delete(tagId);
_eventAggregator.PublishEvent(new TagsUpdatedEvent());
}
private List<int> GetAutoTagIds(Tag tag, List<AutoTag> autoTags)
{
var autoTagIds = autoTags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList();
foreach (var autoTag in autoTags)
{
foreach (var specification in autoTag.Specifications)
{
if (specification is TagSpecification)
{
autoTagIds.Add(autoTag.Id);
}
}
}
return autoTagIds.Distinct().ToList();
}
}
}

View File

@@ -1,6 +1,7 @@
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@@ -51,6 +52,12 @@ namespace NzbDrone.Core.Tv
_logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", series.Title, sourcePath, destinationPath);
}
if (sourcePath.PathEquals(destinationPath))
{
_logger.ProgressInfo("{0} is already in the specified location '{1}'.", series, destinationPath);
return;
}
try
{
// Ensure the parent of the series folder exists, this will often just be the root folder, but

View File

@@ -6,15 +6,16 @@ namespace NzbDrone.Mono.Disk
{
public static class FindDriveType
{
private static readonly Dictionary<string, DriveType> DriveTypeMap = new Dictionary<string, DriveType>
{
{ "afpfs", DriveType.Network },
{ "apfs", DriveType.Fixed },
{ "fuse.mergerfs", DriveType.Fixed },
{ "fuse.glusterfs", DriveType.Network },
{ "nullfs", DriveType.Fixed },
{ "zfs", DriveType.Fixed }
};
private static readonly Dictionary<string, DriveType> DriveTypeMap = new ()
{
{ "afpfs", DriveType.Network },
{ "apfs", DriveType.Fixed },
{ "fuse.mergerfs", DriveType.Fixed },
{ "fuse.shfs", DriveType.Fixed },
{ "fuse.glusterfs", DriveType.Network },
{ "nullfs", DriveType.Fixed },
{ "zfs", DriveType.Fixed }
};
public static DriveType Find(string driveFormat)
{

View File

@@ -5,6 +5,7 @@ using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.CustomFormats;
using Sonarr.Http.REST;
@@ -26,7 +27,7 @@ namespace Sonarr.Api.V3.EpisodeFiles
public List<CustomFormatResource> CustomFormats { get; set; }
public int CustomFormatScore { get; set; }
public int? IndexerFlags { get; set; }
public int? ReleaseType { get; set; }
public ReleaseType? ReleaseType { get; set; }
public MediaInfoResource MediaInfo { get; set; }
public bool QualityCutoffNotMet { get; set; }
@@ -64,7 +65,7 @@ namespace Sonarr.Api.V3.EpisodeFiles
CustomFormats = customFormats.ToResource(false),
CustomFormatScore = customFormatScore,
IndexerFlags = (int)model.IndexerFlags,
ReleaseType = (int)model.ReleaseType,
ReleaseType = model.ReleaseType,
};
}
}

View File

@@ -43,6 +43,7 @@ namespace Sonarr.Api.V3.ManualImport
item.SeasonNumber = processedItem.SeasonNumber;
item.Episodes = processedItem.Episodes.ToResource();
item.ReleaseType = processedItem.ReleaseType;
item.IndexerFlags = processedItem.IndexerFlags;
item.Rejections = processedItem.Rejections;
item.CustomFormats = processedItem.CustomFormats.ToResource(false);

View File

@@ -5143,6 +5143,23 @@
}
}
}
},
"head": {
"tags": [
"Ping"
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PingResource"
}
}
}
}
}
}
},
"/api/v3/qualitydefinition/{id}": {