mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
27 Commits
v4.0.2.140
...
v4.0.3.146
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b8afe3d33 | ||
|
|
476e7a7b94 | ||
|
|
1fcd2b492c | ||
|
|
1aef91041e | ||
|
|
fc06e51352 | ||
|
|
f4c19a384b | ||
|
|
5061dc4b5e | ||
|
|
37863a8deb | ||
|
|
5c42935eb3 | ||
|
|
dac69445e4 | ||
|
|
aca10f6f4f | ||
|
|
74cdf01e49 | ||
|
|
a169ebff2a | ||
|
|
7fc3bebc91 | ||
|
|
e672996dbb | ||
|
|
238ba85f0a | ||
|
|
1562d3bae3 | ||
|
|
7776ec9955 | ||
|
|
af5a681ab7 | ||
|
|
0a7f3a12c2 | ||
|
|
2ef46e5b90 | ||
|
|
6003ca1696 | ||
|
|
0937ee6fef | ||
|
|
60ee7cc716 | ||
|
|
4e83820511 | ||
|
|
5a66b949cf | ||
|
|
f010f56290 |
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal 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
12
.github/dependabot.yml
vendored
Normal 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
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -127,6 +127,7 @@ coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
#VS outout folders
|
||||
bin
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal 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
44
.vscode/tasks.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
53
frontend/src/Components/Form/SeriesTagInput.tsx
Normal file
53
frontend/src/Components/Form/SeriesTagInput.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -15,5 +15,5 @@
|
||||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "minimal-ui"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
17
frontend/src/Episode/getReleaseTypeName.ts
Normal file
17
frontend/src/Episode/getReleaseTypeName.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface InteractiveImportCommandOptions {
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
indexerFlags: number;
|
||||
releaseType: ReleaseType;
|
||||
downloadId?: string;
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function TagInUse(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count > 1 && labelPlural ) {
|
||||
if (count > 1 && labelPlural) {
|
||||
return (
|
||||
<div>
|
||||
{count} {labelPlural.toLowerCase()}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"" }, ")]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -85,7 +85,8 @@ namespace NzbDrone.Core.Annotations
|
||||
Device,
|
||||
TagSelect,
|
||||
RootFolder,
|
||||
QualityProfile
|
||||
QualityProfile,
|
||||
SeriesTag
|
||||
}
|
||||
|
||||
public enum HiddenType
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 usarán todas las opciones.",
|
||||
"IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usarán 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."
|
||||
}
|
||||
|
||||
@@ -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.`)."
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -94,5 +94,15 @@
|
||||
"AudioLanguages": "Мови аудіо",
|
||||
"AuthForm": "Форми (сторінка входу)",
|
||||
"Authentication": "Автентифікація",
|
||||
"AuthenticationMethod": "Метод автентифікації"
|
||||
"AuthenticationMethod": "Метод автентифікації",
|
||||
"Yes": "Так",
|
||||
"AuthenticationRequired": "Потрібна Автентифікація",
|
||||
"UpdateAll": "Оновити все",
|
||||
"WhatsNew": "Що нового ?",
|
||||
"Yesterday": "Вчора",
|
||||
"AddedToDownloadQueue": "Додано в чергу на завантаження",
|
||||
"AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.",
|
||||
"AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5143,6 +5143,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"head": {
|
||||
"tags": [
|
||||
"Ping"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PingResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v3/qualitydefinition/{id}": {
|
||||
|
||||
Reference in New Issue
Block a user