mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
25 Commits
v4.0.5.171
...
v4.0.5.179
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
143ccb1e2a | ||
|
|
29480d9544 | ||
|
|
6de536a7ad | ||
|
|
bce848facf | ||
|
|
ea4fe392a0 | ||
|
|
45fe585944 | ||
|
|
a0d2933134 | ||
|
|
4c622fd412 | ||
|
|
fb060730c7 | ||
|
|
6d5ff9c4d6 | ||
|
|
63bed3e670 | ||
|
|
e684c10432 | ||
|
|
d2509798e9 | ||
|
|
6c39855ebe | ||
|
|
a30e9da767 | ||
|
|
f8e81396d4 | ||
|
|
7fccf590a8 | ||
|
|
e1b937e8d5 | ||
|
|
c331c8bd11 | ||
|
|
52b72925f9 | ||
|
|
378fedcd9d | ||
|
|
a90ab1a8fd | ||
|
|
0edc5ba99a | ||
|
|
ea54ade9bf | ||
|
|
e07eb05e8b |
12
docs.sh
12
docs.sh
@@ -25,17 +25,23 @@ slnFile=src/Sonarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Sonarr.Console.dll
|
||||
else
|
||||
application=Sonarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 &
|
||||
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||
|
||||
sleep 30
|
||||
sleep 45
|
||||
|
||||
kill %1
|
||||
|
||||
|
||||
@@ -217,6 +217,7 @@ class Queue extends Component {
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
maxPageSize={200}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
|
||||
@@ -70,6 +70,11 @@ function QueueStatus(props) {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importBlocked') {
|
||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
|
||||
@@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
};
|
||||
|
||||
onSelect = (value) => {
|
||||
if (Array.isArray(this.props.value)) {
|
||||
let newValue = null;
|
||||
const index = this.props.value.indexOf(value);
|
||||
onSelect = (newValue) => {
|
||||
const { name, value, values, onChange } = this.props;
|
||||
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
let arrayValue = null;
|
||||
const index = value.indexOf(newValue);
|
||||
|
||||
if (index === -1) {
|
||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||
} else {
|
||||
newValue = [...this.props.value];
|
||||
newValue.splice(index, 1);
|
||||
arrayValue = [...value];
|
||||
arrayValue.splice(index, 1);
|
||||
}
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
onChange({
|
||||
name,
|
||||
value: arrayValue,
|
||||
additionalProperties
|
||||
});
|
||||
} else {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
onChange({
|
||||
name,
|
||||
value: newValue,
|
||||
additionalProperties
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component {
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
|
||||
@@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
const importantFieldNames = [
|
||||
'baseUrl',
|
||||
'apiPath',
|
||||
'apiKey'
|
||||
'apiKey',
|
||||
'authToken'
|
||||
];
|
||||
|
||||
function getProviderDataKey(providerData) {
|
||||
@@ -34,7 +35,9 @@ function getSelectOptions(items) {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
parentKey: option.parentValue,
|
||||
isDisabled: option.isDisabled,
|
||||
additionalProperties: option.additionalProperties
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -21,6 +21,7 @@ function createCleanSeriesSelector() {
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tags = []
|
||||
} = series;
|
||||
|
||||
@@ -33,6 +34,7 @@ function createCleanSeriesSelector() {
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
firstCharacter: title.charAt(0).toLowerCase(),
|
||||
tags: tags.reduce((acc, id) => {
|
||||
const matchingTag = allTags.find((tag) => tag.id === id);
|
||||
|
||||
@@ -14,6 +14,7 @@ function SeriesSearchResult(props) {
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tags
|
||||
} = props;
|
||||
|
||||
@@ -73,6 +74,14 @@ function SeriesSearchResult(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'tmdbId' && tmdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
TmdbId: {tmdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
tag ?
|
||||
<div className={styles.tagContainer}>
|
||||
@@ -97,6 +106,7 @@ SeriesSearchResult.propTypes = {
|
||||
tvdbId: PropTypes.number,
|
||||
tvMazeId: PropTypes.number,
|
||||
imdbId: PropTypes.string,
|
||||
tmdbId: PropTypes.number,
|
||||
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
match: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ const fuseOptions = {
|
||||
'tvdbId',
|
||||
'tvMazeId',
|
||||
'imdbId',
|
||||
'tmdbId',
|
||||
'tags.label'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -49,11 +49,12 @@ class TableOptionsModal extends Component {
|
||||
|
||||
onPageSizeChange = ({ value }) => {
|
||||
let pageSizeError = null;
|
||||
const maxPageSize = this.props.maxPageSize ?? 250;
|
||||
|
||||
if (value < 5) {
|
||||
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
|
||||
} else if (value > 250) {
|
||||
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' });
|
||||
} else if (value > maxPageSize) {
|
||||
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` });
|
||||
} else {
|
||||
this.props.onTableOptionChange({ pageSize: value });
|
||||
}
|
||||
@@ -248,6 +249,7 @@ TableOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
pageSize: PropTypes.number,
|
||||
maxPageSize: PropTypes.number,
|
||||
canModifyColumns: PropTypes.bool.isRequired,
|
||||
optionsComponent: PropTypes.elementType,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -14,4 +14,9 @@
|
||||
.deleteFilesMessage {
|
||||
margin-top: 20px;
|
||||
color: var(--dangerColor);
|
||||
|
||||
.deleteCount {
|
||||
margin-top: 20px;
|
||||
color: var(--warningColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteCount': string;
|
||||
'deleteFilesMessage': string;
|
||||
'folderPath': string;
|
||||
'pathContainer': string;
|
||||
|
||||
@@ -50,15 +50,15 @@ class DeleteSeriesModalContent extends Component {
|
||||
const {
|
||||
title,
|
||||
path,
|
||||
statistics,
|
||||
statistics = {},
|
||||
deleteOptions,
|
||||
onModalClose,
|
||||
onDeleteOptionChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
episodeFileCount,
|
||||
sizeOnDisk
|
||||
episodeFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
@@ -108,16 +108,20 @@ class DeleteSeriesModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
deleteFiles &&
|
||||
deleteFiles ?
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div><InlineMarkdown data={translate('DeleteSeriesFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
|
||||
{
|
||||
!!episodeFileCount &&
|
||||
<div>{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
episodeFileCount ?
|
||||
<div className={styles.deleteCount}>
|
||||
{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
||||
@@ -175,6 +175,7 @@ class SeriesDetails extends Component {
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
title,
|
||||
runtime,
|
||||
ratings,
|
||||
@@ -566,6 +567,7 @@ class SeriesDetails extends Component {
|
||||
tvdbId={tvdbId}
|
||||
tvMazeId={tvMazeId}
|
||||
imdbId={imdbId}
|
||||
tmdbId={tmdbId}
|
||||
/>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
@@ -719,6 +721,7 @@ SeriesDetails.propTypes = {
|
||||
tvdbId: PropTypes.number.isRequired,
|
||||
tvMazeId: PropTypes.number,
|
||||
imdbId: PropTypes.string,
|
||||
tmdbId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -9,7 +9,8 @@ function SeriesDetailsLinks(props) {
|
||||
const {
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId
|
||||
imdbId,
|
||||
tmdbId
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -71,6 +72,22 @@ function SeriesDetailsLinks(props) {
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
|
||||
{
|
||||
!!tmdbId &&
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://www.themoviedb.org/tv/${tmdbId}`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
TMDB
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +95,8 @@ function SeriesDetailsLinks(props) {
|
||||
SeriesDetailsLinks.propTypes = {
|
||||
tvdbId: PropTypes.number.isRequired,
|
||||
tvMazeId: PropTypes.number,
|
||||
imdbId: PropTypes.string
|
||||
imdbId: PropTypes.string,
|
||||
tmdbId: PropTypes.number
|
||||
};
|
||||
|
||||
export default SeriesDetailsLinks;
|
||||
|
||||
@@ -10,4 +10,15 @@
|
||||
.path {
|
||||
margin-left: 5px;
|
||||
color: var(--dangerColor);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.statistics {
|
||||
margin-left: 5px;
|
||||
color: var(--warningColor);
|
||||
}
|
||||
|
||||
.deleteFilesMessage {
|
||||
margin-top: 20px;
|
||||
color: var(--warningColor);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteFilesMessage': string;
|
||||
'message': string;
|
||||
'path': string;
|
||||
'pathContainer': string;
|
||||
'statistics': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -16,6 +16,7 @@ import Series from 'Series/Series';
|
||||
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteSeriesModalContent.css';
|
||||
|
||||
@@ -85,6 +86,24 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
|
||||
onModalClose,
|
||||
]);
|
||||
|
||||
const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => {
|
||||
return series.reduce(
|
||||
(acc, s) => {
|
||||
const { statistics = { episodeFileCount: 0, sizeOnDisk: 0 } } = s;
|
||||
const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||
|
||||
acc.totalEpisodeFileCount += episodeFileCount;
|
||||
acc.totalSizeOnDisk += sizeOnDisk;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
totalEpisodeFileCount: 0,
|
||||
totalSizeOnDisk: 0,
|
||||
}
|
||||
);
|
||||
}, [series]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('DeleteSelectedSeries')}</ModalHeader>
|
||||
@@ -137,19 +156,43 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
|
||||
|
||||
<ul>
|
||||
{series.map((s) => {
|
||||
const { episodeFileCount = 0, sizeOnDisk = 0 } = s.statistics;
|
||||
|
||||
return (
|
||||
<li key={s.title}>
|
||||
<span>{s.title}</span>
|
||||
|
||||
{deleteFiles && (
|
||||
<span className={styles.pathContainer}>
|
||||
-<span className={styles.path}>{s.path}</span>
|
||||
<span>
|
||||
<span className={styles.pathContainer}>
|
||||
-<span className={styles.path}>{s.path}</span>
|
||||
</span>
|
||||
|
||||
{!!episodeFileCount && (
|
||||
<span className={styles.statistics}>
|
||||
(
|
||||
{translate('DeleteSeriesFolderEpisodeCount', {
|
||||
episodeFileCount,
|
||||
size: formatBytes(sizeOnDisk),
|
||||
})}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{deleteFiles && !!totalEpisodeFileCount ? (
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
{translate('DeleteSeriesFolderEpisodeCount', {
|
||||
episodeFileCount: totalEpisodeFileCount,
|
||||
size: formatBytes(totalSizeOnDisk),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
||||
@@ -70,6 +70,7 @@ interface Series extends ModelBase {
|
||||
tvdbId: number;
|
||||
tvMazeId: number;
|
||||
tvRageId: number;
|
||||
tmdbId: number;
|
||||
useSceneNumbering: boolean;
|
||||
year: number;
|
||||
isSaving?: boolean;
|
||||
|
||||
@@ -99,6 +99,7 @@ const seriesTokens = [
|
||||
const seriesIdTokens = [
|
||||
{ token: '{ImdbId}', example: 'tt12345' },
|
||||
{ token: '{TvdbId}', example: '12345' },
|
||||
{ token: '{TmdbId}', example: '11223' },
|
||||
{ token: '{TvMazeId}', example: '54321' }
|
||||
];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
saveNotification,
|
||||
setNotificationFieldValue,
|
||||
setNotificationFieldValues,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
@@ -27,7 +27,7 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setNotificationValue,
|
||||
setNotificationFieldValue,
|
||||
setNotificationFieldValues,
|
||||
saveNotification,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
@@ -51,8 +51,8 @@ class EditNotificationModalContentConnector extends Component {
|
||||
this.props.setNotificationValue({ name, value });
|
||||
};
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setNotificationFieldValue({ name, value });
|
||||
onFieldChange = ({ name, value, additionalProperties = {} }) => {
|
||||
this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
@@ -91,7 +91,7 @@ EditNotificationModalContentConnector.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setNotificationValue: PropTypes.func.isRequired,
|
||||
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||
setNotificationFieldValues: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
function createSetProviderFieldValuesReducer(section) {
|
||||
return (state, { payload }) => {
|
||||
if (section === payload.section) {
|
||||
const { properties } = payload;
|
||||
const newState = getSectionState(state, section);
|
||||
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
|
||||
const fields = Object.assign({}, newState.pendingChanges.fields || {});
|
||||
|
||||
Object.keys(properties).forEach((name) => {
|
||||
fields[name] = properties[name];
|
||||
});
|
||||
|
||||
newState.pendingChanges.fields = fields;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
}
|
||||
|
||||
export default createSetProviderFieldValuesReducer;
|
||||
@@ -5,6 +5,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetProviderFieldValuesReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
@@ -22,6 +23,7 @@ export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificati
|
||||
export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema';
|
||||
export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue';
|
||||
export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue';
|
||||
export const SET_NOTIFICATION_FIELD_VALUES = 'settings/notifications/setNotificationFieldValues';
|
||||
export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification';
|
||||
export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification';
|
||||
export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification';
|
||||
@@ -55,6 +57,13 @@ export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VAL
|
||||
};
|
||||
});
|
||||
|
||||
export const setNotificationFieldValues = createAction(SET_NOTIFICATION_FIELD_VALUES, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
@@ -99,6 +108,7 @@ export default {
|
||||
reducers: {
|
||||
[SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||
[SET_NOTIFICATION_FIELD_VALUES]: createSetProviderFieldValuesReducer(section),
|
||||
|
||||
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
|
||||
@@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk
|
||||
{
|
||||
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
|
||||
|
||||
return Directory.EnumerateDirectories(path);
|
||||
return Directory.EnumerateDirectories(path, "*", new EnumerationOptions
|
||||
{
|
||||
AttributesToSkip = FileAttributes.System,
|
||||
IgnoreInaccessible = true
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFiles(string path, bool recursive)
|
||||
|
||||
@@ -366,7 +366,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never());
|
||||
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending);
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportBlocked);
|
||||
}
|
||||
|
||||
private void AssertImported()
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
_singleEpisodeSearchCriteria = new SingleEpisodeSearchCriteria
|
||||
{
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
|
||||
SceneTitles = new List<string> { "Monkey Island" },
|
||||
SeasonNumber = 1,
|
||||
EpisodeNumber = 2
|
||||
@@ -44,14 +44,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
_seasonSearchCriteria = new SeasonSearchCriteria
|
||||
{
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
|
||||
SceneTitles = new List<string> { "Monkey Island" },
|
||||
SeasonNumber = 1,
|
||||
};
|
||||
|
||||
_animeSearchCriteria = new AnimeEpisodeSearchCriteria()
|
||||
{
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
|
||||
SceneTitles = new List<string>() { "Monkey+Island" },
|
||||
AbsoluteEpisodeNumber = 100,
|
||||
SeasonNumber = 5,
|
||||
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
|
||||
_animeSeasonSearchCriteria = new AnimeSeasonSearchCriteria()
|
||||
{
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
|
||||
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
|
||||
SceneTitles = new List<string> { "Monkey Island" },
|
||||
SeasonNumber = 3,
|
||||
};
|
||||
@@ -268,6 +268,19 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||
page.Url.Query.Should().Contain("imdbid=t40");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_search_by_tmdb_if_supported()
|
||||
{
|
||||
_capabilities.SupportedTvSearchParameters = new[] { "q", "tmdbid", "season", "ep" };
|
||||
|
||||
var results = Subject.GetSearchRequests(_singleEpisodeSearchCriteria);
|
||||
results.GetTier(0).Should().HaveCount(1);
|
||||
|
||||
var page = results.GetAllTiers().First().First();
|
||||
|
||||
page.Url.Query.Should().Contain("tmdbid=50");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_prefer_search_by_tvdbid_if_rid_supported()
|
||||
{
|
||||
|
||||
@@ -56,5 +56,14 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
Subject.GetSeriesFolder(_series)
|
||||
.Should().Be($"Series Title ({_series.TvMazeId})");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_tmdb_id()
|
||||
{
|
||||
_namingConfig.SeriesFolderFormat = "{Series Title} ({TmdbId})";
|
||||
|
||||
Subject.GetSeriesFolder(_series)
|
||||
.Should().Be($"Series Title ({_series.TmdbId})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
ExceptionVerification.IgnoreWarns();
|
||||
}
|
||||
|
||||
[TestCase("علم نف) أ.دعادل الأبيض ٢٠٢٤ ٣ ٣")]
|
||||
[TestCase("ror-240618_1007-1022-")]
|
||||
public void should_parse_unknown_formats_without_error(string title)
|
||||
{
|
||||
Parser.Parser.ParseTitle(title).Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_parse_md5()
|
||||
{
|
||||
|
||||
@@ -33,6 +33,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)]
|
||||
[TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)]
|
||||
[TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)]
|
||||
[TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)]
|
||||
[TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)]
|
||||
public void should_parse_full_season_release(string postTitle, string title, int season)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("[www.test-hyphen.ca] - Series (2011) S01", "Series (2011)")]
|
||||
[TestCase("test123.ca - Series Time S02 720p HDTV x264 CRON", "Series Time")]
|
||||
[TestCase("[www.test-hyphen123.co.za] - Series Title S01E01", "Series Title")]
|
||||
[TestCase("(seriesawake.com) Series Super - 57 [720p] [English Subbed]", "Series Super")]
|
||||
|
||||
public void should_not_parse_url_in_name(string postTitle, string title)
|
||||
{
|
||||
|
||||
@@ -137,6 +137,20 @@ namespace NzbDrone.Core.Test.TvTests
|
||||
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TvMazeId == newSeriesInfo.TvMazeId), It.IsAny<bool>(), It.IsAny<bool>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_update_tmdb_id_if_changed()
|
||||
{
|
||||
var newSeriesInfo = _series.JsonClone();
|
||||
newSeriesInfo.TmdbId = _series.TmdbId + 1;
|
||||
|
||||
GivenNewSeriesInfo(newSeriesInfo);
|
||||
|
||||
Subject.Execute(new RefreshSeriesCommand(new List<int> { _series.Id }));
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TmdbId == newSeriesInfo.TmdbId), It.IsAny<bool>(), It.IsAny<bool>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_log_error_if_tvdb_id_not_found()
|
||||
{
|
||||
|
||||
@@ -48,11 +48,11 @@ namespace NzbDrone.Core.Test.UpdateTests
|
||||
{
|
||||
const string branch = "main";
|
||||
UseRealHttp();
|
||||
var recent = Subject.GetRecentUpdates(branch, new Version(3, 0), null);
|
||||
var recent = Subject.GetRecentUpdates(branch, new Version(4, 0), null);
|
||||
|
||||
recent.Should().NotBeEmpty();
|
||||
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
|
||||
recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.3."));
|
||||
recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.4."));
|
||||
recent.Should().OnlyContain(c => c.ReleaseDate.Year >= 2014);
|
||||
recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.New != null);
|
||||
recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.Fixed != null);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace NzbDrone.Core.Annotations
|
||||
@@ -59,13 +60,27 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class FieldSelectOption
|
||||
public class FieldSelectOption<T>
|
||||
where T : struct
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public T Value { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Order { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public int? ParentValue { get; set; }
|
||||
public T? ParentValue { get; set; }
|
||||
public bool? IsDisabled { get; set; }
|
||||
public Dictionary<string, object> AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
public class FieldSelectStringOption
|
||||
{
|
||||
public string Value { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Order { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public string ParentValue { get; set; }
|
||||
public bool? IsDisabled { get; set; }
|
||||
public Dictionary<string, object> AdditionalProperties { get; set; }
|
||||
}
|
||||
|
||||
public enum FieldType
|
||||
|
||||
@@ -419,13 +419,21 @@ namespace NzbDrone.Core.Configuration
|
||||
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it.");
|
||||
}
|
||||
|
||||
return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
|
||||
var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile));
|
||||
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList();
|
||||
|
||||
if (config.Count != 1)
|
||||
{
|
||||
throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Sonarr will recreate it.");
|
||||
}
|
||||
|
||||
return xDoc;
|
||||
}
|
||||
|
||||
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
|
||||
var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
|
||||
newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
|
||||
|
||||
return xDoc;
|
||||
return newXDoc;
|
||||
}
|
||||
}
|
||||
catch (XmlException ex)
|
||||
|
||||
15
src/NzbDrone.Core/Datastore/Migration/206_add_tmdbid.cs
Normal file
15
src/NzbDrone.Core/Datastore/Migration/206_add_tmdbid.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(206)]
|
||||
public class add_tmdbid : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Series").AddColumn("TmdbId").AsInt32().WithDefaultValue(0);
|
||||
Create.Index().OnTable("Series").OnColumn("TmdbId");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ namespace NzbDrone.Core.Download.Aggregation
|
||||
|
||||
public RemoteEpisode Augment(RemoteEpisode remoteEpisode)
|
||||
{
|
||||
if (remoteEpisode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var augmenter in _augmenters)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -124,14 +124,23 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
}
|
||||
|
||||
var items = new List<DownloadClientItem>();
|
||||
var ignoredCount = 0;
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
if (torrent.Hash == null)
|
||||
// Silently ignore torrents with no hash
|
||||
if (torrent.Hash.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore torrents without a name, but track to log a single warning for all invalid torrents.
|
||||
if (torrent.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ignoredCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem();
|
||||
item.DownloadId = torrent.Hash.ToUpper();
|
||||
item.Title = torrent.Name;
|
||||
@@ -189,6 +198,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
if (ignoredCount > 0)
|
||||
{
|
||||
_logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents");
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,8 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
SetImportItem(trackedDownload);
|
||||
|
||||
// Only process tracked downloads that are still downloading
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading)
|
||||
// Only process tracked downloads that are still downloading or have been blocked for importing due to an issue with matching
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -96,7 +96,7 @@ namespace NzbDrone.Core.Download
|
||||
if (series == null)
|
||||
{
|
||||
trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes.");
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
SetStateToImportBlocked(trackedDownload);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ namespace NzbDrone.Core.Download
|
||||
if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch)
|
||||
{
|
||||
trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details.");
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
SetStateToImportBlocked(trackedDownload);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +129,7 @@ namespace NzbDrone.Core.Download
|
||||
if (trackedDownload.RemoteEpisode == null)
|
||||
{
|
||||
trackedDownload.Warn("Unable to parse download, automatic import is not possible.");
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
SetStateToImportBlocked(trackedDownload);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -187,7 +187,7 @@ namespace NzbDrone.Core.Download
|
||||
if (statusMessages.Any())
|
||||
{
|
||||
trackedDownload.Warn(statusMessages.ToArray());
|
||||
SendManualInteractionRequiredNotification(trackedDownload);
|
||||
SetStateToImportBlocked(trackedDownload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,8 +254,10 @@ namespace NzbDrone.Core.Download
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SendManualInteractionRequiredNotification(TrackedDownload trackedDownload)
|
||||
private void SetStateToImportBlocked(TrackedDownload trackedDownload)
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadState.ImportBlocked;
|
||||
|
||||
if (!trackedDownload.HasNotifiedManualInteractionRequired)
|
||||
{
|
||||
var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList();
|
||||
|
||||
@@ -73,8 +73,8 @@ namespace NzbDrone.Core.Download
|
||||
|
||||
public void Check(TrackedDownload trackedDownload)
|
||||
{
|
||||
// Only process tracked downloads that are still downloading
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading)
|
||||
// Only process tracked downloads that are still downloading or import is blocked (if they fail after attempting to be processed)
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
_trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition,
|
||||
downloadItem);
|
||||
|
||||
if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading)
|
||||
if (trackedDownload is { State: TrackedDownloadState.Downloading or TrackedDownloadState.ImportBlocked })
|
||||
{
|
||||
_failedDownloadService.Check(trackedDownload);
|
||||
_completedDownloadService.Check(trackedDownload);
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
public enum TrackedDownloadState
|
||||
{
|
||||
Downloading,
|
||||
ImportBlocked,
|
||||
ImportPending,
|
||||
Importing,
|
||||
Imported,
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
|
||||
public class TrackedDownloadService : ITrackedDownloadService,
|
||||
IHandle<EpisodeInfoRefreshedEvent>,
|
||||
IHandle<SeriesAddedEvent>,
|
||||
IHandle<SeriesDeletedEvent>
|
||||
{
|
||||
private readonly IParsingService _parsingService;
|
||||
@@ -278,12 +279,29 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(SeriesAddedEvent message)
|
||||
{
|
||||
var cachedItems = _cache.Values
|
||||
.Where(t =>
|
||||
t.RemoteEpisode?.Series == null ||
|
||||
message.Series?.TvdbId == t.RemoteEpisode.Series.TvdbId)
|
||||
.ToList();
|
||||
|
||||
if (cachedItems.Any())
|
||||
{
|
||||
cachedItems.ForEach(UpdateCachedItem);
|
||||
|
||||
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads()));
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(SeriesDeletedEvent message)
|
||||
{
|
||||
var cachedItems = _cache.Values.Where(t =>
|
||||
t.RemoteEpisode?.Series != null &&
|
||||
message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id))
|
||||
.ToList();
|
||||
var cachedItems = _cache.Values
|
||||
.Where(t =>
|
||||
t.RemoteEpisode?.Series != null &&
|
||||
message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id || s.TvdbId == t.RemoteEpisode.Series.TvdbId))
|
||||
.ToList();
|
||||
|
||||
if (cachedItems.Any())
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
|
||||
Tvdb = series.TvdbId.ToString();
|
||||
TvMaze = series.TvMazeId > 0 ? series.TvMazeId.ToString() : null;
|
||||
TvRage = series.TvRageId > 0 ? series.TvMazeId.ToString() : null;
|
||||
Tmdb = series.TmdbId > 0 ? series.TmdbId.ToString() : null;
|
||||
Imdb = series.ImdbId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace NzbDrone.Core.ImportLists.Imdb
|
||||
// Parse TSV response from IMDB export
|
||||
var rows = importResponse.Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
series = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 1).SelectList(i => new ImportListItemInfo { ImdbId = i[1] });
|
||||
series = rows.Skip(1).SelectList(m => m.Split(',')).Where(m => m.Length > 5).SelectList(i => new ImportListItemInfo { ImdbId = i[1], Title = i[5] });
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public static class NewznabCategoryFieldOptionsConverter
|
||||
{
|
||||
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
|
||||
public static List<FieldSelectOption<int>> GetFieldSelectOptions(List<NewznabCategory> categories)
|
||||
{
|
||||
// Categories not relevant for Sonarr
|
||||
var ignoreCategories = new[] { 1000, 3000, 4000, 6000, 7000 };
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
// And maybe relevant for specific users
|
||||
var unimportantCategories = new[] { 0, 2000 };
|
||||
|
||||
var result = new List<FieldSelectOption>();
|
||||
var result = new List<FieldSelectOption<int>>();
|
||||
|
||||
if (categories == null)
|
||||
{
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
|
||||
foreach (var category in categories.Where(cat => !ignoreCategories.Contains(cat.Id)).OrderBy(cat => unimportantCategories.Contains(cat.Id)).ThenBy(cat => cat.Id))
|
||||
{
|
||||
result.Add(new FieldSelectOption
|
||||
result.Add(new FieldSelectOption<int>
|
||||
{
|
||||
Value = category.Id,
|
||||
Name = category.Name,
|
||||
@@ -52,7 +52,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
foreach (var subcat in category.Subcategories.OrderBy(cat => cat.Id))
|
||||
{
|
||||
result.Add(new FieldSelectOption
|
||||
result.Add(new FieldSelectOption<int>
|
||||
{
|
||||
Value = subcat.Id,
|
||||
Name = subcat.Name,
|
||||
|
||||
@@ -119,12 +119,23 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
}
|
||||
}
|
||||
|
||||
private bool SupportsTmdbSearch
|
||||
{
|
||||
get
|
||||
{
|
||||
var capabilities = _capabilitiesProvider.GetCapabilities(Settings);
|
||||
|
||||
return capabilities.SupportedTvSearchParameters != null &&
|
||||
capabilities.SupportedTvSearchParameters.Contains("tmdbid");
|
||||
}
|
||||
}
|
||||
|
||||
// Combines all ID based searches
|
||||
private bool SupportsTvIdSearches
|
||||
{
|
||||
get
|
||||
{
|
||||
return SupportsTvdbSearch || SupportsImdbSearch || SupportsTvRageSearch || SupportsTvMazeSearch;
|
||||
return SupportsTvdbSearch || SupportsImdbSearch || SupportsTvRageSearch || SupportsTvMazeSearch || SupportsTmdbSearch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,8 +495,9 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
var includeImdbSearch = SupportsImdbSearch && searchCriteria.Series.ImdbId.IsNotNullOrWhiteSpace();
|
||||
var includeTvRageSearch = SupportsTvRageSearch && searchCriteria.Series.TvRageId > 0;
|
||||
var includeTvMazeSearch = SupportsTvMazeSearch && searchCriteria.Series.TvMazeId > 0;
|
||||
var includeTmdbSearch = SupportsTmdbSearch && searchCriteria.Series.TmdbId > 0;
|
||||
|
||||
if (SupportsAggregatedIdSearch && (includeTvdbSearch || includeTvRageSearch || includeTvMazeSearch))
|
||||
if (SupportsAggregatedIdSearch && (includeTvdbSearch || includeTvRageSearch || includeTvMazeSearch || includeTmdbSearch))
|
||||
{
|
||||
var ids = "";
|
||||
|
||||
@@ -509,6 +521,11 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
ids += "&tvmazeid=" + searchCriteria.Series.TvMazeId;
|
||||
}
|
||||
|
||||
if (includeTmdbSearch)
|
||||
{
|
||||
ids += "&tmdbid=" + searchCriteria.Series.TmdbId;
|
||||
}
|
||||
|
||||
chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch", ids + parameters));
|
||||
}
|
||||
else
|
||||
@@ -541,6 +558,13 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
"tvsearch",
|
||||
$"&tvmazeid={searchCriteria.Series.TvMazeId}{parameters}"));
|
||||
}
|
||||
else if (includeTmdbSearch)
|
||||
{
|
||||
chain.Add(GetPagedRequests(MaxPages,
|
||||
categories,
|
||||
"tvsearch",
|
||||
$"&tmdbid={searchCriteria.Series.TmdbId}{parameters}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,16 +68,17 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(TorrentEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Torznab indexer?", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("{1} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, NzbEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -262,26 +262,26 @@ namespace NzbDrone.Core.Indexers
|
||||
protected virtual RssEnclosure[] GetEnclosures(XElement item)
|
||||
{
|
||||
var enclosures = item.Elements("enclosure")
|
||||
.Select(v =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new RssEnclosure
|
||||
{
|
||||
Url = v.Attribute("url")?.Value,
|
||||
Type = v.Attribute("type")?.Value,
|
||||
Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warn(e, "Failed to get enclosure for: {0}", item.Title());
|
||||
}
|
||||
.Select(v =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new RssEnclosure
|
||||
{
|
||||
Url = v.Attribute("url")?.Value,
|
||||
Type = v.Attribute("type")?.Value,
|
||||
Length = v.Attribute("length")?.Value?.ParseInt64() ?? 0
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to get enclosure for: {0}", item.Title());
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.Where(v => v != null)
|
||||
.ToArray();
|
||||
return null;
|
||||
})
|
||||
.Where(v => v != null)
|
||||
.ToArray();
|
||||
|
||||
return enclosures;
|
||||
}
|
||||
|
||||
@@ -59,16 +59,17 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||
protected override bool PostProcess(IndexerResponse indexerResponse, List<XElement> items, List<ReleaseInfo> releases)
|
||||
{
|
||||
var enclosureTypes = items.SelectMany(GetEnclosures).Select(v => v.Type).Distinct().ToArray();
|
||||
|
||||
if (enclosureTypes.Any() && enclosureTypes.Intersect(PreferredEnclosureMimeTypes).Empty())
|
||||
{
|
||||
if (enclosureTypes.Intersect(UsenetEnclosureMimeTypes).Any())
|
||||
{
|
||||
_logger.Warn("{0} does not contain {1}, found {2}, did you intend to add a Newznab indexer?", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("{1} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
_logger.Warn("{0} does not contain {1}, found {2}.", indexerResponse.Request.Url, TorrentEnclosureMimeType, enclosureTypes[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -34,5 +34,9 @@
|
||||
"AddImportListImplementation": "Tilføj importliste - {implementationName}",
|
||||
"AddRootFolderError": "Kunne ikke tilføje rodmappe",
|
||||
"Table": "Tabel",
|
||||
"AddIndexer": "Tilføj indekser"
|
||||
"AddIndexer": "Tilføj indekser",
|
||||
"AddDownloadClient": "Tilføj downloadklient",
|
||||
"AddImportListExclusion": "Tilføj ekslusion til importeringslisten",
|
||||
"AddDelayProfileError": "Kan ikke tilføje en ny forsinkelsesprofil. Prøv venligst igen.",
|
||||
"AddDownloadClientError": "Ikke muligt at tilføje en ny downloadklient. Prøv venligst igen."
|
||||
}
|
||||
|
||||
@@ -1361,6 +1361,8 @@
|
||||
"NotificationsNtfyValidationAuthorizationRequired": "Authorization is required",
|
||||
"NotificationsPlexSettingsAuthToken": "Auth Token",
|
||||
"NotificationsPlexSettingsAuthenticateWithPlexTv": "Authenticate with Plex.tv",
|
||||
"NotificationsPlexSettingsServer": "Server",
|
||||
"NotificationsPlexSettingsServerHelpText": "Select server from plex.tv account after authenticating",
|
||||
"NotificationsPlexValidationNoTvLibraryFound": "At least one TV library is required",
|
||||
"NotificationsPushBulletSettingSenderId": "Sender ID",
|
||||
"NotificationsPushBulletSettingSenderIdHelpText": "The device ID to send notifications from, use device_iden in the device's URL on pushbullet.com (leave blank to send from yourself)",
|
||||
@@ -1992,6 +1994,7 @@
|
||||
"Umask770Description": "{octal} - Owner & Group write",
|
||||
"Umask775Description": "{octal} - Owner & Group write, Other read",
|
||||
"Umask777Description": "{octal} - Everyone write",
|
||||
"UnableToImportAutomatically": "Unable to Import Automatically",
|
||||
"UnableToLoadAutoTagging": "Unable to load auto tagging",
|
||||
"UnableToLoadBackups": "Unable to load backups",
|
||||
"UnableToUpdateSonarrDirectly": "Unable to update {appName} directly,",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Added": "Añadido",
|
||||
"ApplyChanges": "Aplicar Cambios",
|
||||
"AuthBasic": "Básico (ventana emergente del navegador)",
|
||||
"AuthBasic": "Básica (Ventana emergente del navegador)",
|
||||
"BackupFolderHelpText": "Las rutas relativas estarán en el directorio AppData de {appName}",
|
||||
"BackupsLoadError": "No se pudieron cargar las copias de seguridad",
|
||||
"Enable": "Habilitar",
|
||||
@@ -36,22 +36,22 @@
|
||||
"AddNewRestriction": "Añadir nueva restricción",
|
||||
"AddRemotePathMapping": "Añadir Asignación de Ruta Remota",
|
||||
"Analytics": "Analíticas",
|
||||
"ApiKey": "Clave de API",
|
||||
"ApiKey": "Clave API",
|
||||
"AnimeEpisodeFormat": "Formato de Episodio de Anime",
|
||||
"ApplicationUrlHelpText": "La URL externa de la aplicación incluyendo http(s)://, puerto y URL base",
|
||||
"ApplyTagsHelpTextReplace": "Reemplazar: Sustituye las etiquetas por las introducidas (introduce \"no tags\" para borrar todas las etiquetas)",
|
||||
"ApplicationURL": "URL de la aplicación",
|
||||
"Authentication": "Autenticación",
|
||||
"AuthForm": "Formularios (Página de inicio de sesión)",
|
||||
"AuthenticationMethodHelpText": "Requerir nombre de usuario y contraseña para acceder {appName}",
|
||||
"AuthenticationMethodHelpText": "Requiere usuario y contraseña para acceder a {appName}",
|
||||
"AuthenticationRequired": "Autenticación requerida",
|
||||
"AutoTaggingLoadError": "No se pudo cargar el etiquetado automático",
|
||||
"AutoRedownloadFailedHelpText": "Buscar e intentar descargar automáticamente una versión diferente",
|
||||
"AutoRedownloadFailedHelpText": "Busca e intenta descargar automáticamente una versión diferente",
|
||||
"Backup": "Copia de seguridad",
|
||||
"AutomaticSearch": "Búsqueda Automática",
|
||||
"Automatic": "Automático",
|
||||
"BindAddressHelpText": "Dirección IP4 válida, localhost o '*' para todas las interfaces",
|
||||
"BindAddress": "Dirección de Ligado",
|
||||
"BindAddress": "Dirección de enlace",
|
||||
"Branch": "Rama",
|
||||
"BuiltIn": "Integrado",
|
||||
"Condition": "Condición",
|
||||
@@ -65,7 +65,7 @@
|
||||
"Duplicate": "Duplicar",
|
||||
"Error": "Error",
|
||||
"Episodes": "Episodios",
|
||||
"External": "Externo",
|
||||
"External": "Externa",
|
||||
"Extend": "Extender",
|
||||
"Restore": "Restaurar",
|
||||
"Security": "Seguridad",
|
||||
@@ -78,10 +78,10 @@
|
||||
"Torrents": "Torrents",
|
||||
"Ui": "Interfaz",
|
||||
"Underscore": "Guion bajo",
|
||||
"UpdateMechanismHelpText": "Usar el actualizador integrado de {appName} o un script",
|
||||
"UpdateMechanismHelpText": "Usa el actualizador integrado de {appName} o un script",
|
||||
"Warn": "Advertencia",
|
||||
"AutoTagging": "Etiquetado Automático",
|
||||
"AddAutoTag": "Añadir Etiqueta Automática",
|
||||
"AddAutoTag": "Añadir etiqueta automática",
|
||||
"AddCondition": "Añadir Condición",
|
||||
"AbsoluteEpisodeNumbers": "Número(s) de Episodio Absoluto(s)",
|
||||
"AirDate": "Fecha de Emisión",
|
||||
@@ -165,7 +165,7 @@
|
||||
"AddRemotePathMappingError": "No se pudo añadir una nueva asignación de ruta remota, inténtelo de nuevo.",
|
||||
"AgeWhenGrabbed": "Antigüedad (cuando se añadió)",
|
||||
"AllResultsAreHiddenByTheAppliedFilter": "Todos los resultados están ocultos por el filtro aplicado",
|
||||
"AnalyseVideoFilesHelpText": "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.",
|
||||
"AnalyseVideoFilesHelpText": "Extrae 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.",
|
||||
"AnimeEpisodeTypeDescription": "Episodios lanzados usando un número de episodio absoluto",
|
||||
"ApiKeyValidationHealthCheckMessage": "Por favor actualiza tu clave API para que tenga de longitud al menos {length} caracteres. Puedes hacerlo en los ajustes o en el archivo de configuración",
|
||||
"AppDataLocationHealthCheckMessage": "No será posible actualizar para evitar la eliminación de AppData al actualizar",
|
||||
@@ -174,7 +174,7 @@
|
||||
"Clone": "Clonar",
|
||||
"Connections": "Conexiones",
|
||||
"Dash": "Guion",
|
||||
"AnalyticsEnabledHelpText": "Envíe información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre su navegador, qué páginas de {appName} WebUI utiliza, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.",
|
||||
"AnalyticsEnabledHelpText": "Envía información anónima de uso y error a los servidores de {appName}. Esto incluye información sobre tu navegador, qué páginas de interfaz web de {appName} utilizas, informes de errores, así como el sistema operativo y la versión en tiempo de ejecución. Usaremos esta información para priorizar funciones y correcciones de errores.",
|
||||
"BackupIntervalHelpText": "Intervalo entre copias de seguridad automáticas",
|
||||
"BackupRetentionHelpText": "Las copias de seguridad automáticas anteriores al período de retención serán borradas automáticamente",
|
||||
"AddNewSeries": "Añadir Nueva Serie",
|
||||
@@ -182,7 +182,7 @@
|
||||
"AddNewSeriesHelpText": "Es fácil añadir una nueva serie, empiece escribiendo el nombre de la serie que desea añadir.",
|
||||
"AddNewSeriesRootFolderHelpText": "La subcarpeta '{folder}' será creada automáticamente",
|
||||
"AddNewSeriesSearchForCutoffUnmetEpisodes": "Empezar la búsqueda de episodios con límites no alcanzados",
|
||||
"AddNewSeriesSearchForMissingEpisodes": "Empezar búsqueda de episodios faltantes",
|
||||
"AddNewSeriesSearchForMissingEpisodes": "Empezar la búsqueda de episodios faltantes",
|
||||
"AddQualityProfile": "Añadir Perfil de Calidad",
|
||||
"AddQualityProfileError": "No se pudo añadir un nuevo perfil de calidad, inténtelo de nuevo.",
|
||||
"AddReleaseProfile": "Añadir perfil de lanzamiento",
|
||||
@@ -199,7 +199,7 @@
|
||||
"EditSelectedDownloadClients": "Editar Clientes de Descarga Seleccionados",
|
||||
"DeleteRemotePathMappingMessageText": "¿Está seguro de querer eliminar esta asignación de ruta remota?",
|
||||
"Implementation": "Implementación",
|
||||
"ImportUsingScript": "Importar Script de Uso",
|
||||
"ImportUsingScript": "Importar usando un script",
|
||||
"CloneAutoTag": "Clonar Etiquetado Automático",
|
||||
"ManageIndexers": "Gestionar Indexadores",
|
||||
"DeleteAutoTag": "Eliminar Etiquetado Automático",
|
||||
@@ -219,15 +219,15 @@
|
||||
"AddConditionImplementation": "Añadir condición - {implementationName}",
|
||||
"AppUpdated": "{appName} Actualizado",
|
||||
"AutomaticUpdatesDisabledDocker": "Las actualizaciones automáticas no están soportadas directamente cuando se utiliza el mecanismo de actualización de Docker. Tendrá que actualizar la imagen del contenedor fuera de {appName} o utilizar un script",
|
||||
"AuthenticationRequiredHelpText": "Cambiar para que las solicitudes requieran autenticación. No lo cambie a menos que entienda los riesgos.",
|
||||
"AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación esté habilitada. Opcionalmente puede desactivar la autenticación desde una dirección local.",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Introduzca una nueva contraseña",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Introduzca un nuevo nombre de usuario",
|
||||
"AuthenticationRequiredHelpText": "Cambia para qué solicitudes se requiere autenticación. No cambiar a menos que entiendas los riesgos.",
|
||||
"AuthenticationRequiredWarning": "Para evitar el acceso remoto sin autenticación, {appName} ahora requiere que la autenticación sea habilitada. Opcionalmente puedes deshabilitar la autenticación desde direcciones locales.",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Introduce una nueva contraseña",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Introduce un nuevo usuario",
|
||||
"AuthenticationMethod": "Método de autenticación",
|
||||
"AddConnectionImplementation": "Añadir Conexión - {implementationName}",
|
||||
"AddDownloadClientImplementation": "Añadir Cliente de Descarga - {implementationName}",
|
||||
"VideoDynamicRange": "Video de Rango Dinámico",
|
||||
"AuthenticationMethodHelpTextWarning": "Por favor selecciona un método válido de autenticación",
|
||||
"AuthenticationMethodHelpTextWarning": "Por favor selecciona un método de autenticación válido",
|
||||
"AddCustomFilter": "Añadir Filtro Personalizado",
|
||||
"BypassDelayIfAboveCustomFormatScoreMinimumScore": "Puntuación mínima de formato personalizado",
|
||||
"CountIndexersSelected": "{count} indexador(es) seleccionado(s)",
|
||||
@@ -266,12 +266,12 @@
|
||||
"DeleteSelectedImportListsMessageText": "¿Estás seguro que quieres eliminar {count} lista(s) de importación seleccionada(s)?",
|
||||
"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",
|
||||
"DisabledForLocalAddresses": "Deshabilitada para direcciones locales",
|
||||
"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",
|
||||
"MonitorRecentEpisodesDescription": "Monitorizar episodios emitidos en los últimos 90 días y los episodios futuros",
|
||||
"MonitorRecentEpisodesDescription": "Monitoriza episodios emitidos en los últimos 90 días y los episodios futuros",
|
||||
"MonitorSelected": "Monitorizar seleccionados",
|
||||
"MonitorSeries": "Monitorizar Series",
|
||||
"NoHistory": "Sin historial",
|
||||
@@ -288,7 +288,7 @@
|
||||
"MonitorNoNewSeasonsDescription": "No monitorizar automáticamente ninguna temporada nueva",
|
||||
"HistoryLoadError": "No se pudo cargar el historial",
|
||||
"LibraryImport": "Importar Librería",
|
||||
"RescanSeriesFolderAfterRefresh": "Re-escanear la Carpeta de Series tras Actualizar",
|
||||
"RescanSeriesFolderAfterRefresh": "Volver a escanear la carpeta de series tras actualizar",
|
||||
"Wanted": "Buscado",
|
||||
"MonitorPilotEpisodeDescription": "Sólo monitorizar el primer episodio de la primera temporada",
|
||||
"MonitorRecentEpisodes": "Episodios Recientes",
|
||||
@@ -300,14 +300,14 @@
|
||||
"MonitorSpecialEpisodes": "Monitorizar Especiales",
|
||||
"Queue": "Cola",
|
||||
"RescanAfterRefreshHelpTextWarning": "{appName} no detectará automáticamente cambios en los archivos si no se elige 'Siempre'",
|
||||
"RescanAfterRefreshSeriesHelpText": "Re-escanear la carpeta de series tras actualizar las series",
|
||||
"RescanAfterRefreshSeriesHelpText": "Vuelve a escanear la carpeta de series tras actualizar las series",
|
||||
"MonitorNoNewSeasons": "Sin Nuevas Temporadas",
|
||||
"MonitorSpecialEpisodesDescription": "Monitorizar todos los episodios especiales sin cambiar el estado de monitorizado de otros episodios",
|
||||
"MonitorSpecialEpisodesDescription": "Monitoriza todos los episodios especiales sin cambiar el estado de monitorizado de otros episodios",
|
||||
"Calendar": "Calendario",
|
||||
"BlocklistRelease": "Lista de bloqueos de lanzamiento",
|
||||
"CountSeasons": "{count} Temporadas",
|
||||
"BranchUpdate": "Rama a usar para actualizar {appName}",
|
||||
"ChmodFolder": "Carpeta chmod",
|
||||
"ChmodFolder": "chmod de la carpeta",
|
||||
"CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles",
|
||||
"ChooseAnotherFolder": "Elige otra Carpeta",
|
||||
"ClientPriority": "Prioridad del Cliente",
|
||||
@@ -317,7 +317,7 @@
|
||||
"CalendarLoadError": "Incapaz de cargar el calendario",
|
||||
"CertificateValidation": "Validacion de certificado",
|
||||
"BypassProxyForLocalAddresses": "Omitir Proxy para Direcciones Locales",
|
||||
"ChangeFileDateHelpText": "Cambiar la fecha del archivo al importar/rescan",
|
||||
"ChangeFileDateHelpText": "Cambia la fecha del archivo al importar/volver a escanear",
|
||||
"ChownGroupHelpText": "Nombre del grupo o gid. Utilice gid para sistemas de archivos remotos.",
|
||||
"CloneProfile": "Clonar Perfil",
|
||||
"CollectionsLoadError": "No se han podido cargar las colecciones",
|
||||
@@ -355,7 +355,7 @@
|
||||
"Agenda": "Agenda",
|
||||
"Cancel": "Cancelar",
|
||||
"ChangeFileDate": "Cambiar fecha de archivo",
|
||||
"CertificateValidationHelpText": "Cambiar como es la validacion de la certificacion estricta de HTTPS. No cambiar a menos que entiendas las consecuencias.",
|
||||
"CertificateValidationHelpText": "Cambia cómo de estricta es la validación de certificación de HTTPS. No cambiar a menos que entiendas los riesgos.",
|
||||
"AddListExclusion": "Añadir lista de exclusión",
|
||||
"AddedDate": "Agregado: {date}",
|
||||
"AllSeriesAreHiddenByTheAppliedFilter": "Todos los resultados estan ocultos por el filtro aplicado",
|
||||
@@ -422,7 +422,7 @@
|
||||
"FailedToFetchUpdates": "Fallo al buscar las actualizaciones",
|
||||
"FailedToUpdateSettings": "Fallo al actualizar los ajustes",
|
||||
"MaintenanceRelease": "Lanzamiento de mantenimiento: Corrección de errores y otras mejoras. Ver historial de commits de Github para mas detalle",
|
||||
"CreateEmptySeriesFoldersHelpText": "Cree carpetas de series faltantes durante el análisis del disco",
|
||||
"CreateEmptySeriesFoldersHelpText": "Crea carpetas de series faltantes durante el análisis del disco",
|
||||
"DefaultCase": "Caso predeterminado",
|
||||
"Daily": "Diario",
|
||||
"CollapseMultipleEpisodesHelpText": "Contraer varios episodios que se emiten el mismo día",
|
||||
@@ -465,7 +465,7 @@
|
||||
"InteractiveImportNoQuality": "La calidad debe elegirse para cada archivo seleccionado",
|
||||
"InteractiveSearchModalHeader": "Búsqueda interactiva",
|
||||
"InvalidUILanguage": "Su interfaz de usuario está configurada en un idioma no válido, corríjalo y guarde la configuración",
|
||||
"ChownGroup": "chown grupo",
|
||||
"ChownGroup": "chown del grupo",
|
||||
"DelayProfileProtocol": "Protocolo: {preferredProtocol}",
|
||||
"DelayProfilesLoadError": "Incapaz de cargar Perfiles de Retardo",
|
||||
"ContinuingSeriesDescription": "Se esperan más episodios u otra temporada",
|
||||
@@ -598,7 +598,7 @@
|
||||
"DownloadClientNzbgetValidationKeepHistoryZero": "La configuración KeepHistory de NzbGet debería ser mayor de 0",
|
||||
"DownloadClientNzbgetValidationKeepHistoryZeroDetail": "La configuración KeepHistory de NzbGet está establecida a 0. Esto evita que {appName} vea las descargas completadas.",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissing": "La carpeta compartida no existe",
|
||||
"DownloadPropersAndRepacksHelpText": "Decidir si automáticamente actualizar a Propers/Repacks",
|
||||
"DownloadPropersAndRepacksHelpText": "Actualiza automáticamente o no a Propers/Repacks",
|
||||
"EditListExclusion": "Editar exclusión de lista",
|
||||
"EnableAutomaticAdd": "Habilitar añadido automático",
|
||||
"EditQualityProfile": "Editar perfil de calidad",
|
||||
@@ -737,7 +737,7 @@
|
||||
"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",
|
||||
"EnableCompletedDownloadHandlingHelpText": "Importa automáticamente las descargas completas del gestor de descargas",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Buscar no está soportado por este indexador",
|
||||
"EnableRss": "Habilitar RSS",
|
||||
"Ended": "Terminado",
|
||||
@@ -783,7 +783,7 @@
|
||||
"FilterDoesNotEndWith": "no termina en",
|
||||
"Fixed": "Arreglado",
|
||||
"Global": "Global",
|
||||
"Enabled": "Habilitado",
|
||||
"Enabled": "Habilitada",
|
||||
"EpisodeHistoryLoadError": "No se puede cargar el historial del episodio",
|
||||
"EpisodeIsDownloading": "El episodio se está descargando",
|
||||
"EpisodeHasNotAired": "El episodio no está en emisión",
|
||||
@@ -801,7 +801,7 @@
|
||||
"GeneralSettings": "Opciones generales",
|
||||
"GeneralSettingsLoadError": "No se pueden cargar las opciones generales",
|
||||
"Grab": "Capturar",
|
||||
"GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} no pudo determinar para qué serie y episodio era este lanzamiento. {appName} no pudo automáticamente importar este lanzamiento. ¿Te gustaría capturar '{title}'?",
|
||||
"GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} no pudo determinar para qué serie y episodio era este lanzamiento. {appName} no pudo importar automáticamente este lanzamiento. ¿Te gustaría capturar '{title}'?",
|
||||
"HasMissingSeason": "Tiene temporadas faltantes",
|
||||
"ImportListSearchForMissingEpisodesHelpText": "Una vez se añada la serie a {appName}, buscar automáticamente episodios faltantes",
|
||||
"ImportListsSonarrValidationInvalidUrl": "La URL de {appName} es inválida. ¿Te falta la URL base?",
|
||||
@@ -989,7 +989,7 @@
|
||||
"ImportListsValidationInvalidApiKey": "La clave API es inválida",
|
||||
"ImportListsValidationTestFailed": "El test fue abortado debido a un error: {exceptionMessage}",
|
||||
"ImportScriptPathHelpText": "La ruta al script a usar para importar",
|
||||
"ImportUsingScriptHelpText": "Copiar archivos para importar usando un script (p. ej. para transcodificación)",
|
||||
"ImportUsingScriptHelpText": "Copia archivos para importar usando un script (p. ej. para transcodificación)",
|
||||
"Importing": "Importando",
|
||||
"IncludeUnmonitored": "Incluir no monitorizadas",
|
||||
"IndexerLongTermStatusAllUnavailableHealthCheckMessage": "Ningún indexador está disponible debido a errores durante más de 6 horas",
|
||||
@@ -1040,7 +1040,7 @@
|
||||
"ImportMechanismHandlingDisabledHealthCheckMessage": "Habilitar Gestión de descargas completadas",
|
||||
"ImportedTo": "Importar a",
|
||||
"IncludeCustomFormatWhenRenaming": "Incluir formato personalizado cuando se renombra",
|
||||
"CleanLibraryLevel": "Limpiar el nivel de la librería",
|
||||
"CleanLibraryLevel": "Limpiar nivel de biblioteca",
|
||||
"SearchForCutoffUnmetEpisodes": "Buscar todos los episodios con límites no alcanzados",
|
||||
"IconForSpecials": "Icono para Especiales",
|
||||
"ImportListExclusions": "Importar lista de exclusiones",
|
||||
@@ -1067,7 +1067,7 @@
|
||||
"IndexerSearchNoAutomaticHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Automática activada, {appName} no proporcionará ningún resultado de búsquedas automáticas",
|
||||
"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",
|
||||
"PasswordConfirmation": "Confirmación de contraseña",
|
||||
"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",
|
||||
@@ -1189,7 +1189,7 @@
|
||||
"ListSyncTag": "Etiqueta de Sincronización de Lista",
|
||||
"ListSyncTagHelpText": "Esta etiqueta se añadirá cuando una serie desaparezca o ya no esté en su(s) lista(s)",
|
||||
"MetadataLoadError": "No se puede cargar Metadatos",
|
||||
"MetadataSourceSettingsSeriesSummary": "Información de dónde {appName} obtiene información de series y episodio",
|
||||
"MetadataSourceSettingsSeriesSummary": "Fuente de información de donde {appName} obtiene información de series y episodios",
|
||||
"Max": "Máximo",
|
||||
"MaximumSizeHelpText": "Tamaño máximo en MB para que un lanzamiento sea capturado. Establece a cero para establecer a ilimitado",
|
||||
"MatchedToEpisodes": "Ajustado a Episodios",
|
||||
@@ -1486,7 +1486,7 @@
|
||||
"RemoveQueueItemConfirmation": "¿Estás seguro que quieres eliminar '{sourceTitle}' de la cola?",
|
||||
"RemoveRootFolder": "Eliminar la carpeta raíz",
|
||||
"RemoveSelectedItem": "Eliminar elemento seleccionado",
|
||||
"RemoveTagsAutomaticallyHelpText": "Eliminar etiquetas automáticamente si las condiciones no se cumplen",
|
||||
"RemoveTagsAutomaticallyHelpText": "Elimina etiquetas automáticamente si las condiciones no se cumplen",
|
||||
"RemovedFromTaskQueue": "Eliminar de la cola de tareas",
|
||||
"RemovedSeriesMultipleRemovedHealthCheckMessage": "Las series {series} fueron eliminadas de TheTVDB",
|
||||
"RenameFiles": "Renombrar archivos",
|
||||
@@ -1595,7 +1595,7 @@
|
||||
"TestAllIndexers": "Probar todos los indexadores",
|
||||
"TestAllLists": "Probar todas las listas",
|
||||
"TestParsing": "Probar análisis",
|
||||
"ThemeHelpText": "Cambiar el tema de la interfaz de la aplicación, el tema 'Auto' usará el tema de tu sistema para establecer el modo luminoso u oscuro. Inspirado por Theme.Park",
|
||||
"ThemeHelpText": "Cambia el tema de la interfaz de la aplicación, el tema 'Auto' usará el tema de tu sistema para establecer el modo luminoso u oscuro. Inspirado por Theme.Park",
|
||||
"TimeLeft": "Tiempo restante",
|
||||
"ToggleMonitoredSeriesUnmonitored ": "No se puede conmutar el estado monitorizado cuando la serie no está monitorizada",
|
||||
"Tomorrow": "Mañana",
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"Upcoming": "Próximamente",
|
||||
"UpcomingSeriesDescription": "Series que han sido anunciadas pero aún no hay fecha de emisión exacta",
|
||||
"ReleaseSceneIndicatorUnknownSeries": "Episodio o serie desconocido.",
|
||||
"RemoveDownloadsAlert": "Las opciones de Eliminar fueron movidas a las opciones del cliente de descarga individual en la table anterior.",
|
||||
"RemoveDownloadsAlert": "Las opciones de eliminación fueron trasladadas a las opciones del cliente de descarga individual en la tabla anterior.",
|
||||
"RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto",
|
||||
"SelectFolder": "Seleccionar carpeta",
|
||||
"TestAllClients": "Probar todos los clientes",
|
||||
@@ -1894,7 +1894,7 @@
|
||||
"UiLanguage": "Idioma de interfaz",
|
||||
"UiLanguageHelpText": "Idioma que {appName} usará en la interfaz",
|
||||
"UiSettingsSummary": "Opciones de calendario, fecha y color alterado",
|
||||
"UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones",
|
||||
"UpdateAutomaticallyHelpText": "Descarga e instala actualizaciones automáticamente. Podrás seguir instalándolas desde Sistema: Actualizaciones",
|
||||
"TotalRecords": "Total de registros: {totalRecords}",
|
||||
"WantMoreControlAddACustomFormat": "¿Quieres más control sobre qué descargas son preferidas? Añade un [formato personalizado](/opciones/formatospersonalizados)",
|
||||
"OrganizeModalHeader": "Organizar y renombrar",
|
||||
@@ -1916,7 +1916,7 @@
|
||||
"SelectEpisodesModalTitle": "{modalTitle} - Seleccionar episodio(s)",
|
||||
"DownloadClientDelugeSettingsDirectory": "Directorio de descarga",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Deluge",
|
||||
"UnmonitorSpecialsEpisodesDescription": "Dejar de monitorizar todos los episodios especiales sin cambiar el estado monitorizado de otros episodios",
|
||||
"UnmonitorSpecialsEpisodesDescription": "Deja de monitorizar todos los episodios especiales sin cambiar el estado monitorizado de otros episodios",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicación opcional a la que mover las descargas completadas, dejar en blanco para usar la ubicación predeterminada de Deluge",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Directorio al que mover cuando se complete",
|
||||
"NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord",
|
||||
|
||||
@@ -250,5 +250,32 @@
|
||||
"AutoRedownloadFailed": "Download fallito",
|
||||
"AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.",
|
||||
"Cutoff": "Taglio",
|
||||
"AddListExclusion": "Aggiungi elenco esclusioni"
|
||||
"AddListExclusion": "Aggiungi elenco esclusioni",
|
||||
"DownloadClientValidationApiKeyRequired": "API Key Richiesta",
|
||||
"Donate": "Dona",
|
||||
"DownloadClientDownloadStationValidationNoDefaultDestination": "Nessuna destinazione predefinita",
|
||||
"ImportListSettings": "Impostazioni delle Liste",
|
||||
"DownloadClientFreeboxSettingsAppId": "App ID",
|
||||
"DownloadClientFreeboxSettingsAppToken": "App Token",
|
||||
"DownloadClientPneumaticSettingsNzbFolderHelpText": "Questa cartella dovrà essere raggiungibile da XBMC",
|
||||
"DownloadClientPneumaticSettingsNzbFolder": "Cartella Nzb",
|
||||
"DownloadClientSabnzbdValidationEnableDisableDateSorting": "Disattiva ordinamento per data",
|
||||
"DownloadClientQbittorrentValidationCategoryUnsupported": "Categoria non è supportata",
|
||||
"DownloadClientValidationCategoryMissing": "Categoria non esiste",
|
||||
"DownloadClientRTorrentSettingsUrlPath": "Percorso Url",
|
||||
"Default": "Predefinito",
|
||||
"DownloadClientFreeboxSettingsApiUrl": "API URL",
|
||||
"DownloadClientQbittorrentValidationCategoryRecommended": "Categoria è raccomandata",
|
||||
"Discord": "Discord",
|
||||
"DownloadClientDownloadStationValidationFolderMissing": "Cartella non esiste",
|
||||
"DownloadClientValidationAuthenticationFailure": "Autenticazione Fallita",
|
||||
"DownloadClientDownloadStationValidationFolderMissingDetail": "La cartella '{downloadDir}' non esiste, deve essere creata manualmente all'interno della Cartella Condivisa '{sharedFolder}'.",
|
||||
"DownloadClientSabnzbdValidationUnknownVersion": "Versione sconosciuta: {rawVersion}",
|
||||
"DownloadClientValidationVerifySsl": "Verifica impostazioni SSL",
|
||||
"ChangeCategory": "Cambia Categoria",
|
||||
"DownloadClientPneumaticSettingsStrmFolder": "Cartella Strm",
|
||||
"Destination": "Destinazione",
|
||||
"DownloadClientDownloadStationValidationSharedFolderMissing": "Cartella condivisa non esiste",
|
||||
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} non tenterà di importare i download completati senza una categoria.",
|
||||
"DownloadClientValidationGroupMissing": "Gruppo non esistente"
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"Calendar": "Calendário",
|
||||
"Connect": "Conectar",
|
||||
"CustomFormats": "Formatos personalizados",
|
||||
"CutoffUnmet": "Limite não alcançado",
|
||||
"CutoffUnmet": "Corte Não Alcançado",
|
||||
"DownloadClients": "Clientes de download",
|
||||
"Events": "Eventos",
|
||||
"General": "Geral",
|
||||
@@ -432,7 +432,7 @@
|
||||
"AuthBasic": "Básico (pop-up do navegador)",
|
||||
"AuthForm": "Formulário (página de login)",
|
||||
"Authentication": "Autenticação",
|
||||
"AuthenticationMethodHelpText": "Exigir nome de usuário e senha para acessar o {appName}",
|
||||
"AuthenticationMethodHelpText": "Exigir Nome de Usuário e Senha para acessar {appName}",
|
||||
"AuthenticationRequired": "Autenticação exigida",
|
||||
"AutoRedownloadFailedHelpText": "Procurar automaticamente e tente baixar uma versão diferente",
|
||||
"AutoTaggingLoadError": "Não foi possível carregar tagging automática",
|
||||
@@ -494,7 +494,7 @@
|
||||
"CustomFormatsSettings": "Configurações de Formatos Personalizados",
|
||||
"CustomFormatsSettingsSummary": "Configurações e Formatos Personalizados",
|
||||
"DailyEpisodeFormat": "Formato do episódio diário",
|
||||
"Cutoff": "Limite",
|
||||
"Cutoff": "Corte",
|
||||
"Dash": "Traço",
|
||||
"Dates": "Datas",
|
||||
"Debug": "Depuração",
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"AppUpdated": "{appName} Güncellendi",
|
||||
"ApplicationURL": "Uygulama URL'si",
|
||||
"ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır?",
|
||||
"ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır",
|
||||
"ApplyTagsHelpTextRemove": "Kaldır: Girilen etiketleri kaldırın",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Yeni şifre girin",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Yeni kullanıcı adınızı girin",
|
||||
@@ -88,8 +88,8 @@
|
||||
"CustomFormatUnknownConditionOption": "'{implementation}' koşulu için bilinmeyen seçenek '{key}'",
|
||||
"AutoTagging": "Otomatik Etiketleme",
|
||||
"AutoTaggingNegateHelpText": "İşaretlenirse, {implementationName} koşulu eşleştiğinde otomatik etiketleme kuralı uygulanmayacaktır.",
|
||||
"ApplyTagsHelpTextHowToApplyDownloadClients": "Seçilen indirme istemcilerine etiketler nasıl uygulanır?",
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "Seçilen içe aktarma listelerine etiketler nasıl uygulanır?",
|
||||
"ApplyTagsHelpTextHowToApplyDownloadClients": "Seçilen indirme istemcilerine etiketler nasıl uygulanır",
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "Seçilen içe aktarma listelerine etiketler nasıl uygulanır",
|
||||
"AuthenticationRequiredHelpText": "İstekler için Kimlik doğrulamanın gereklilik ayarını değiştirin. Riskleri anlamadığınız sürece değiştirmeyin.",
|
||||
"AutoTaggingLoadError": "Otomatik etiketleme yüklenemiyor",
|
||||
"BypassDelayIfAboveCustomFormatScore": "Özel Format Koşullarının Üstündeyse Baypas Et",
|
||||
@@ -837,5 +837,14 @@
|
||||
"UpdateAutomaticallyHelpText": "Güncelleştirmeleri otomatik olarak indirip yükleyin. Sistem: Güncellemeler'den yükleme yapmaya devam edebileceksiniz",
|
||||
"Wanted": "Arananlar",
|
||||
"Cutoff": "Kesinti",
|
||||
"Required": "Gerekli"
|
||||
"Required": "Gerekli",
|
||||
"AirsTbaOn": "Daha sonra duyurulacak {networkLabel}'de",
|
||||
"AllFiles": "Tüm dosyalar",
|
||||
"AllSeriesAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir",
|
||||
"Always": "Her zaman",
|
||||
"AirsDateAtTimeOn": "{date} saat {time} {networkLabel}'de",
|
||||
"AllResultsAreHiddenByTheAppliedFilter": "Tüm sonuçlar, uygulanan filtre tarafından gizlenir",
|
||||
"AllSeriesInRootFolderHaveBeenImported": "{path} içerisindeki tüm diziler içeri aktarıldı",
|
||||
"AlternateTitles": "Alternatif Başlıklar",
|
||||
"AnEpisodeIsDownloading": "Bir bölüm indiriliyor"
|
||||
}
|
||||
|
||||
@@ -550,7 +550,7 @@
|
||||
"AutoRedownloadFailedHelpText": "自动搜索并尝试下载不同的版本",
|
||||
"AutoTaggingLoadError": "无法加载自动标记",
|
||||
"Automatic": "自动化",
|
||||
"AutoTaggingRequiredHelpText": "这个{implementationName}条件必须匹配自动标记规则才能应用。否则,一个{implementationName}匹配就足够了。",
|
||||
"AutoTaggingRequiredHelpText": "这个{0}条件必须匹配自动标记规则才能应用。否则,一个{0}匹配就足够了。",
|
||||
"AutoTaggingNegateHelpText": "如果选中,当 {implementationName} 条件匹配时,自动标记不会应用。",
|
||||
"BackupRetentionHelpText": "超过保留期限的自动备份将被自动清理",
|
||||
"BackupsLoadError": "无法加载备份",
|
||||
|
||||
@@ -137,6 +137,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
||||
public string LastAired { get; set; }
|
||||
public int? TvRageId { get; set; }
|
||||
public int? TvMazeId { get; set; }
|
||||
public int? TmdbId { get; set; }
|
||||
|
||||
public string Status { get; set; }
|
||||
public int? Runtime { get; set; }
|
||||
|
||||
@@ -188,6 +188,11 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
series.TvMazeId = show.TvMazeId.Value;
|
||||
}
|
||||
|
||||
if (show.TmdbId.HasValue)
|
||||
{
|
||||
series.TmdbId = show.TmdbId.Value;
|
||||
}
|
||||
|
||||
series.ImdbId = show.ImdbId;
|
||||
series.Title = show.Title;
|
||||
series.CleanTitle = Parser.Parser.CleanSeriesTitle(show.Title);
|
||||
|
||||
@@ -69,6 +69,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_TitleSlug", series.TitleSlug);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
@@ -116,6 +117,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
@@ -181,6 +183,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
@@ -213,6 +216,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
@@ -252,6 +256,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
@@ -276,6 +281,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
@@ -345,6 +351,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_Series_Path", series.Path);
|
||||
environmentVariables.Add("Sonarr_Series_TvdbId", series.TvdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TvMazeId", series.TvMazeId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_TmdbId", series.TmdbId.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_ImdbId", series.ImdbId ?? string.Empty);
|
||||
environmentVariables.Add("Sonarr_Series_Type", series.SeriesType.ToString());
|
||||
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace NzbDrone.Core.Notifications.Emby
|
||||
public string Imdb { get; set; }
|
||||
public int Tvdb { get; set; }
|
||||
public int TvMaze { get; set; }
|
||||
public int Tmdb { get; set; }
|
||||
public int TvRage { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ namespace NzbDrone.Core.Notifications.Emby
|
||||
return MediaBrowserMatchQuality.Id;
|
||||
}
|
||||
|
||||
if (item is { ProviderIds.Tmdb: int tmdbid } && tmdbid != 0 && tmdbid == series.TmdbId)
|
||||
{
|
||||
return MediaBrowserMatchQuality.Id;
|
||||
}
|
||||
|
||||
if (item is { ProviderIds.TvRage: int tvrageid } && tvrageid != 0 && tvrageid == series.TvRageId)
|
||||
{
|
||||
return MediaBrowserMatchQuality.Id;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
@@ -12,6 +13,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||
{
|
||||
string GetAuthToken(string clientIdentifier, int pinId);
|
||||
bool Ping(string clientIdentifier, string authToken);
|
||||
List<PlexTvResource> GetResources(string clientIdentifier, string authToken);
|
||||
}
|
||||
|
||||
public class PlexTvProxy : IPlexTvProxy
|
||||
@@ -62,6 +64,33 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<PlexTvResource> GetResources(string clientIdentifier, string authToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Allows us to tell plex.tv that we're still active and tokens should not be expired.
|
||||
|
||||
var request = BuildRequest(clientIdentifier);
|
||||
|
||||
request.ResourceUrl = "/api/v2/resources";
|
||||
request.AddQueryParam("includeHttps", 1);
|
||||
request.AddQueryParam("clientID", clientIdentifier);
|
||||
request.AddQueryParam("X-Plex-Token", authToken);
|
||||
|
||||
if (Json.TryDeserialize<List<PlexTvResource>>(ProcessRequest(request), out var response))
|
||||
{
|
||||
return response;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catch all exceptions and log at trace, this information could be interesting in debugging, but expired tokens will be handled elsewhere.
|
||||
_logger.Trace(e, "Unable to ping plex.tv");
|
||||
}
|
||||
|
||||
return new List<PlexTvResource>();
|
||||
}
|
||||
|
||||
private HttpRequestBuilder BuildRequest(string clientIdentifier)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder("https://plex.tv")
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||
{
|
||||
public class PlexTvResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool Owned { get; set; }
|
||||
|
||||
public List<PlexTvResourceConnection> Connections { get; set; }
|
||||
|
||||
[JsonProperty("provides")]
|
||||
public string ProvidesRaw { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> Provides => ProvidesRaw.Split(",").ToList();
|
||||
}
|
||||
|
||||
public class PlexTvResourceConnection
|
||||
{
|
||||
public string Uri { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int Port { get; set; }
|
||||
public bool Local { get; set; }
|
||||
public string Host => Uri.IsNullOrWhiteSpace() ? Address : new Uri(Uri).Host;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using NzbDrone.Common.Cache;
|
||||
@@ -14,6 +15,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||
PlexTvSignInUrlResponse GetSignInUrl(string callbackUrl, int pinId, string pinCode);
|
||||
string GetAuthToken(int pinId);
|
||||
void Ping(string authToken);
|
||||
List<PlexTvResource> GetServers(string authToken);
|
||||
HttpRequest GetWatchlist(string authToken, int pageSize, int pageOffset);
|
||||
}
|
||||
|
||||
@@ -93,6 +95,16 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
|
||||
_cache.Get(authToken, () => _proxy.Ping(_configService.PlexClientIdentifier, authToken), TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
public List<PlexTvResource> GetServers(string authToken)
|
||||
{
|
||||
Ping(authToken);
|
||||
|
||||
var clientIdentifier = _configService.PlexClientIdentifier;
|
||||
var resources = _proxy.GetResources(clientIdentifier, authToken);
|
||||
|
||||
return resources.Where(r => r.Owned && r.Provides.Contains("server")).ToList();
|
||||
}
|
||||
|
||||
public HttpRequest GetWatchlist(string authToken, int pageSize, int pageOffset)
|
||||
{
|
||||
Ping(authToken);
|
||||
|
||||
@@ -5,6 +5,7 @@ using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Notifications.Plex.PlexTv;
|
||||
@@ -193,6 +194,79 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
||||
};
|
||||
}
|
||||
|
||||
if (action == "servers")
|
||||
{
|
||||
Settings.Validate().Filter("AuthToken").ThrowOnError();
|
||||
|
||||
if (Settings.AuthToken.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new { };
|
||||
}
|
||||
|
||||
var servers = _plexTvService.GetServers(Settings.AuthToken);
|
||||
var options = servers.SelectMany(s =>
|
||||
{
|
||||
var result = new List<FieldSelectStringOption>();
|
||||
|
||||
// result.Add(new FieldSelectStringOption
|
||||
// {
|
||||
// Value = s.Name,
|
||||
// Name = s.Name,
|
||||
// IsDisabled = true
|
||||
// });
|
||||
|
||||
s.Connections.ForEach(c =>
|
||||
{
|
||||
var isSecure = c.Protocol == "https";
|
||||
var additionalProperties = new Dictionary<string, object>();
|
||||
var hints = new List<string>();
|
||||
|
||||
additionalProperties.Add("host", c.Host);
|
||||
additionalProperties.Add("port", c.Port);
|
||||
additionalProperties.Add("useSsl", isSecure);
|
||||
hints.Add(c.Local ? "Local" : "Remote");
|
||||
|
||||
if (isSecure)
|
||||
{
|
||||
hints.Add("Secure");
|
||||
}
|
||||
|
||||
result.Add(new FieldSelectStringOption
|
||||
{
|
||||
Value = c.Uri,
|
||||
Name = $"{s.Name} ({c.Host})",
|
||||
Hint = string.Join(", ", hints),
|
||||
AdditionalProperties = additionalProperties
|
||||
});
|
||||
|
||||
if (isSecure)
|
||||
{
|
||||
var uri = $"http://{c.Address}:{c.Port}";
|
||||
var insecureAdditionalProperties = new Dictionary<string, object>();
|
||||
|
||||
insecureAdditionalProperties.Add("host", c.Address);
|
||||
insecureAdditionalProperties.Add("port", c.Port);
|
||||
insecureAdditionalProperties.Add("useSsl", false);
|
||||
|
||||
result.Add(new FieldSelectStringOption
|
||||
{
|
||||
Value = uri,
|
||||
Name = $"{s.Name} ({c.Address})",
|
||||
Hint = c.Local ? "Local" : "Remote",
|
||||
AdditionalProperties = insecureAdditionalProperties
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return new
|
||||
{
|
||||
options
|
||||
};
|
||||
}
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
@@ -22,40 +23,45 @@ namespace NzbDrone.Core.Notifications.Plex.Server
|
||||
|
||||
public PlexServerSettings()
|
||||
{
|
||||
Host = "";
|
||||
Port = 32400;
|
||||
UpdateLibrary = true;
|
||||
SignIn = "startOAuth";
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Host")]
|
||||
[JsonIgnore]
|
||||
[FieldDefinition(0, Label = "NotificationsPlexSettingsServer", Type = FieldType.Select, SelectOptionsProviderAction = "servers", HelpText = "NotificationsPlexSettingsServerHelpText")]
|
||||
public string Server { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Host")]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Port")]
|
||||
[FieldDefinition(2, Label = "Port")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "NotificationsSettingsUseSslHelpText")]
|
||||
[FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "NotificationsSettingsUseSslHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "UseSsl", "serviceName", "Plex")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")]
|
||||
[FieldDefinition(4, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "ConnectionSettingsUrlBaseHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "connectionName", "Plex")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]/plex")]
|
||||
public string UrlBase { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)]
|
||||
[FieldDefinition(5, Label = "NotificationsPlexSettingsAuthToken", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, Advanced = true)]
|
||||
public string AuthToken { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)]
|
||||
[FieldDefinition(6, Label = "NotificationsPlexSettingsAuthenticateWithPlexTv", Type = FieldType.OAuth)]
|
||||
public string SignIn { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)]
|
||||
[FieldDefinition(7, Label = "NotificationsSettingsUpdateLibrary", Type = FieldType.Checkbox)]
|
||||
public bool UpdateLibrary { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")]
|
||||
[FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsFrom", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsFromHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsFrom", "serviceName", "Plex")]
|
||||
public string MapFrom { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")]
|
||||
[FieldDefinition(9, Label = "NotificationsSettingsUpdateMapPathsTo", Type = FieldType.Textbox, Advanced = true, HelpText = "NotificationsSettingsUpdateMapPathsToHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "NotificationsSettingsUpdateMapPathsTo", "serviceName", "Plex")]
|
||||
public string MapTo { get; set; }
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
public string Path { get; set; }
|
||||
public int TvdbId { get; set; }
|
||||
public int TvMazeId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public SeriesTypes Type { get; set; }
|
||||
public int Year { get; set; }
|
||||
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
Path = series.Path;
|
||||
TvdbId = series.TvdbId;
|
||||
TvMazeId = series.TvMazeId;
|
||||
TmdbId = series.TmdbId;
|
||||
ImdbId = series.ImdbId;
|
||||
Type = series.SeriesType;
|
||||
Year = series.Year;
|
||||
|
||||
@@ -715,6 +715,7 @@ namespace NzbDrone.Core.Organizer
|
||||
tokenHandlers["{ImdbId}"] = m => series.ImdbId ?? string.Empty;
|
||||
tokenHandlers["{TvdbId}"] = m => series.TvdbId.ToString();
|
||||
tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty;
|
||||
tokenHandlers["{TmdbId}"] = m => series.TmdbId > 0 ? series.TmdbId.ToString() : string.Empty;
|
||||
}
|
||||
|
||||
private string GetCustomFormatsToken(List<CustomFormat> customFormats, string filter)
|
||||
|
||||
@@ -48,7 +48,8 @@ namespace NzbDrone.Core.Organizer
|
||||
Year = 2010,
|
||||
ImdbId = "tt12345",
|
||||
TvdbId = 12345,
|
||||
TvMazeId = 54321
|
||||
TvMazeId = 54321,
|
||||
TmdbId = 11223
|
||||
};
|
||||
|
||||
_dailySeries = new Series
|
||||
@@ -58,7 +59,8 @@ namespace NzbDrone.Core.Organizer
|
||||
Year = 2010,
|
||||
ImdbId = "tt12345",
|
||||
TvdbId = 12345,
|
||||
TvMazeId = 54321
|
||||
TvMazeId = 54321,
|
||||
TmdbId = 11223
|
||||
};
|
||||
|
||||
_animeSeries = new Series
|
||||
@@ -68,7 +70,8 @@ namespace NzbDrone.Core.Organizer
|
||||
Year = 2010,
|
||||
ImdbId = "tt12345",
|
||||
TvdbId = 12345,
|
||||
TvMazeId = 54321
|
||||
TvMazeId = 54321,
|
||||
TmdbId = 11223
|
||||
};
|
||||
|
||||
_episode1 = new Episode
|
||||
|
||||
@@ -126,6 +126,10 @@ namespace NzbDrone.Core.Parser
|
||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
// Anime - [SubGroup] Title with trailing number S## (Full season)
|
||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+(?:S(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))).+?(?:$|\.mkv)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
// Anime - [SubGroup] Title Absolute Episode Number
|
||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
@@ -510,7 +514,7 @@ namespace NzbDrone.Core.Parser
|
||||
|
||||
// Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt
|
||||
|
||||
private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*\]|[ -]{2,})[ -]*",
|
||||
private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^(?:(?:\[|\()\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?<!Naruto-Kun\.)(?:[a-z]{2,6}\.[a-z]{2,6}|xn--[a-z0-9-]{4,}|[a-z]{2,})\b(?:\s*(?:\]|\))|[ -]{2,})[ -]*",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
@@ -1205,7 +1209,7 @@ namespace NzbDrone.Core.Parser
|
||||
}
|
||||
}
|
||||
|
||||
if (lastSeasonEpisodeStringIndex != releaseTitle.Length)
|
||||
if (lastSeasonEpisodeStringIndex < releaseTitle.Length)
|
||||
{
|
||||
result.ReleaseTokens = releaseTitle.Substring(lastSeasonEpisodeStringIndex);
|
||||
}
|
||||
@@ -1285,7 +1289,7 @@ namespace NzbDrone.Core.Parser
|
||||
|
||||
private static int ParseNumber(string value)
|
||||
{
|
||||
var normalized = value.Normalize(NormalizationForm.FormKC);
|
||||
var normalized = ConvertToNumerals(value.Normalize(NormalizationForm.FormKC));
|
||||
|
||||
if (int.TryParse(normalized, out var number))
|
||||
{
|
||||
@@ -1304,7 +1308,7 @@ namespace NzbDrone.Core.Parser
|
||||
|
||||
private static decimal ParseDecimal(string value)
|
||||
{
|
||||
var normalized = value.Normalize(NormalizationForm.FormKC);
|
||||
var normalized = ConvertToNumerals(value.Normalize(NormalizationForm.FormKC));
|
||||
|
||||
if (decimal.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out var number))
|
||||
{
|
||||
@@ -1313,5 +1317,24 @@ namespace NzbDrone.Core.Parser
|
||||
|
||||
throw new FormatException(string.Format("{0} isn't a number", value));
|
||||
}
|
||||
|
||||
private static string ConvertToNumerals(string input)
|
||||
{
|
||||
var result = new StringBuilder(input.Length);
|
||||
|
||||
foreach (var c in input.ToCharArray())
|
||||
{
|
||||
if (char.IsNumber(c))
|
||||
{
|
||||
result.Append(char.GetNumericValue(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ namespace NzbDrone.Core.Tv
|
||||
|
||||
private void MoveSingleSeries(Series series, string sourcePath, string destinationPath, int? index = null, int? total = null)
|
||||
{
|
||||
if (!sourcePath.IsPathValid(PathValidationType.CurrentOs))
|
||||
{
|
||||
_logger.Warn("Folder '{0}' for '{1}' is invalid, unable to move series. Try moving files manually", sourcePath, series.Title);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderExists(sourcePath))
|
||||
{
|
||||
_logger.Debug("Folder '{0}' for '{1}' does not exist, not moving.", sourcePath, series.Title);
|
||||
|
||||
@@ -92,6 +92,7 @@ namespace NzbDrone.Core.Tv
|
||||
series.TitleSlug = seriesInfo.TitleSlug;
|
||||
series.TvRageId = seriesInfo.TvRageId;
|
||||
series.TvMazeId = seriesInfo.TvMazeId;
|
||||
series.TmdbId = seriesInfo.TmdbId;
|
||||
series.ImdbId = seriesInfo.ImdbId;
|
||||
series.AirTime = seriesInfo.AirTime;
|
||||
series.Overview = seriesInfo.Overview;
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Tv
|
||||
public int TvRageId { get; set; }
|
||||
public int TvMazeId { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string CleanTitle { get; set; }
|
||||
public string SortTitle { get; set; }
|
||||
|
||||
@@ -58,12 +58,12 @@ namespace NzbDrone.Core.Tv
|
||||
}
|
||||
else
|
||||
{
|
||||
if (series.AddOptions.SearchForMissingEpisodes)
|
||||
if (addOptions.SearchForMissingEpisodes)
|
||||
{
|
||||
_commandQueueManager.Push(new MissingEpisodeSearchCommand(series.Id));
|
||||
}
|
||||
|
||||
if (series.AddOptions.SearchForCutoffUnmetEpisodes)
|
||||
if (addOptions.SearchForCutoffUnmetEpisodes)
|
||||
{
|
||||
_commandQueueManager.Push(new CutoffUnmetEpisodeSearchCommand(series.Id));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentValidation.Validators;
|
||||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
|
||||
@@ -24,7 +25,7 @@ namespace NzbDrone.Core.Validation.Paths
|
||||
|
||||
context.MessageFormatter.AppendArgument("path", context.PropertyValue.ToString());
|
||||
|
||||
return !_rootFolderService.All().Exists(r => r.Path.PathEquals(context.PropertyValue.ToString()));
|
||||
return !_rootFolderService.All().Exists(r => r.Path.IsPathValid(PathValidationType.CurrentOs) && r.Path.PathEquals(context.PropertyValue.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
@@ -28,7 +29,10 @@ namespace NzbDrone.Core.Validation.Paths
|
||||
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||
var instanceId = (int)instance.Id;
|
||||
|
||||
return !_seriesService.GetAllSeriesPaths().Any(s => s.Value.PathEquals(context.PropertyValue.ToString()) && s.Key != instanceId);
|
||||
// Skip the path for this series and any invalid paths
|
||||
return !_seriesService.GetAllSeriesPaths().Any(s => s.Key != instanceId &&
|
||||
s.Value.IsPathValid(PathValidationType.CurrentOs) &&
|
||||
s.Value.PathEquals(context.PropertyValue.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.21" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.2" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.Tv;
|
||||
using Sonarr.Api.V3.EpisodeFiles;
|
||||
using Sonarr.Api.V3.Series;
|
||||
using Sonarr.Http.REST;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace Sonarr.Api.V3.Episodes
|
||||
{
|
||||
@@ -33,13 +34,13 @@ namespace Sonarr.Api.V3.Episodes
|
||||
public bool UnverifiedSceneNumbering { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
public DateTime? GrabDate { get; set; }
|
||||
public string SeriesTitle { get; set; }
|
||||
public SeriesResource Series { get; set; }
|
||||
|
||||
public List<MediaCover> Images { get; set; }
|
||||
|
||||
// Hiding this so people don't think its usable (only used to set the initial state)
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
[SwaggerIgnore]
|
||||
public bool Grabbed { get; set; }
|
||||
}
|
||||
|
||||
@@ -77,7 +78,6 @@ namespace Sonarr.Api.V3.Episodes
|
||||
SceneEpisodeNumber = model.SceneEpisodeNumber,
|
||||
SceneSeasonNumber = model.SceneSeasonNumber,
|
||||
UnverifiedSceneNumbering = model.UnverifiedSceneNumbering,
|
||||
SeriesTitle = model.SeriesTitle,
|
||||
|
||||
// Series = model.Series.MapToResource(),
|
||||
};
|
||||
|
||||
@@ -86,7 +86,8 @@ namespace Sonarr.Api.V3.Indexers
|
||||
{
|
||||
if (release.IndexerId == 0 && release.Indexer.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name == release.Indexer);
|
||||
var indexer = _indexerFactory.All().FirstOrDefault(v => v.Name.EqualsIgnoreCase(release.Indexer));
|
||||
|
||||
if (indexer != null)
|
||||
{
|
||||
release.IndexerId = indexer.Id;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
@@ -32,7 +33,7 @@ namespace Sonarr.Api.V3
|
||||
_bulkResourceMapper = bulkResourceMapper;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique");
|
||||
SharedValidator.RuleFor(c => c.Name).Must((v, c) => !_providerFactory.All().Any(p => p.Name.EqualsIgnoreCase(c) && p.Id != v.Id)).WithMessage("Should be unique");
|
||||
SharedValidator.RuleFor(c => c.Implementation).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty();
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace Sonarr.Api.V3.Series
|
||||
public int TvdbId { get; set; }
|
||||
public int TvRageId { get; set; }
|
||||
public int TvMazeId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public DateTime? FirstAired { get; set; }
|
||||
public DateTime? LastAired { get; set; }
|
||||
public SeriesTypes SeriesType { get; set; }
|
||||
@@ -123,6 +124,7 @@ namespace Sonarr.Api.V3.Series
|
||||
TvdbId = model.TvdbId,
|
||||
TvRageId = model.TvRageId,
|
||||
TvMazeId = model.TvMazeId,
|
||||
TmdbId = model.TmdbId,
|
||||
FirstAired = model.FirstAired,
|
||||
LastAired = model.LastAired,
|
||||
SeriesType = model.SeriesType,
|
||||
@@ -187,6 +189,7 @@ namespace Sonarr.Api.V3.Series
|
||||
TvdbId = resource.TvdbId,
|
||||
TvRageId = resource.TvRageId,
|
||||
TvMazeId = resource.TvMazeId,
|
||||
TmdbId = resource.TmdbId,
|
||||
FirstAired = resource.FirstAired,
|
||||
SeriesType = resource.SeriesType,
|
||||
CleanTitle = resource.CleanTitle,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="Ical.Net" Version="4.2.0" />
|
||||
<PackageReference Include="NLog" Version="4.7.14" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user