1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

Compare commits

...

31 Commits

Author SHA1 Message Date
Mark McDowall 98c737a146 New: Move auth success logging to debug
Closes #7978
2025-08-10 21:15:05 -07:00
Stevie Robinson f0798550af New Include Finale type in Webhook and Custom Script connections
Closes #7999
2025-08-10 21:10:48 -07:00
Mark McDowall d9c7838329 Fixed: Sub group parsing could result in extra brackets being parsed
Closes #7994
2025-08-10 21:10:11 -07:00
Mark McDowall b00229e53c Fixed: Treat TaoE and QxR as release group instead of encoder
Closes #7972
2025-08-10 21:10:11 -07:00
Luigi 880628fb68 New: Select with poster click in series selection 2025-08-10 21:09:50 -07:00
Stevie Robinson b09c6f0811 New: Include Mal and AniList IDs in API response and Webhooks
Closes #7973
2025-08-10 21:08:25 -07:00
Mark McDowall b376b63c9e New: Parse '(JA)' as Japanese
Closes #7956
2025-08-10 21:07:32 -07:00
bparkin1283 99feaa34d2 Replace service --status-all with systemctl is-active 2025-08-10 21:07:27 -07:00
Stevie Robinson d7f82a72c2 Fixed: Update nzb.su domain to nzb.life 2025-08-10 21:06:41 -07:00
Stevie Robinson bd20ebfad7 New: Indexer option for Season Pack Seed Ratio 2025-08-10 21:06:20 -07:00
jutoft 71553ad67b New: Tribler 8 download client
Closes #1813
2025-08-10 21:05:40 -07:00
Weblate 41c39f1f28 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Dino <me@dinodev.org>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mrchonks <chonkstv@gmail.com>
Co-authored-by: myrad2267 <myrad2267@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-08-10 21:04:57 -07:00
Mark McDowall d0066358eb Upgraded SixLabors.ImageSharp to 3.1.11 2025-08-01 09:31:00 -07:00
Weblate 6f1d461dad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: wavybox <leeviervoemil@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translation: Servarr/Sonarr
2025-07-07 17:38:19 -07:00
Stevie Robinson 6ccab3cfc8 Fixed: Improve parsing of bit depth 2025-07-07 17:37:45 -07:00
Mark McDowall 5e47cc3baa Fixed: Improve parsing of 4-digit absolute numbering batches 2025-07-07 17:37:19 -07:00
Mark McDowall 78ca30d1f8 Don't log debug messages for API key validation
Closes #7934
2025-07-07 17:37:10 -07:00
Mark McDowall f9d0abada3 Fixed: Manual Import could lead to duplicate notifications
Closes #7922
2025-07-07 17:37:02 -07:00
Mark McDowall 4bdb0408f1 Return error if Manual Import called without items
Closes #7942
2025-07-07 17:36:53 -07:00
Stevie Robinson 40ea6ce4e5 New: Persist queue removal option in browser
Closes #7938
2025-07-07 17:36:45 -07:00
nuxen ccf33033dc Fixed: xvid not always detected correctly 2025-07-07 17:36:06 -07:00
Mark McDowall 996c0e9f50 Upgrade MonoTorrent to 3.0.2
Closes #7270
2025-07-07 17:35:37 -07:00
Stevie Robinson 8b7f9daab0 Improve Emby/Jellyfin connection test 2025-07-07 17:35:32 -07:00
Mark McDowall dfb6fdfbeb Change authentication to Forms if set to Basic 2025-07-07 17:34:34 -07:00
Mark McDowall 29d0073ee6 Make remove root folder Sonarr specific 2025-07-07 17:34:34 -07:00
Mark McDowall 9cf6be32fa Fixed deleting tags from UI 2025-07-07 17:34:34 -07:00
Ben Martin fee3f8150e New: Option to add series tags when adding torrents to qbit
Closes #7930
2025-07-07 17:34:25 -07:00
nuxen 010bbbd222 Support for multiple seriesIds in Rename API endpoint 2025-07-07 17:33:40 -07:00
ricecrackerfiend d3c3a6ebce Fixed: Prevent long series titles from cutting off synopsis
Closes #7875
2025-07-07 17:33:15 -07:00
Stevie Robinson f26344ae75 New: Allow user to define additional rejected extensions
Closes #7721
2025-07-07 17:32:34 -07:00
Weblate 034f731308 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Gjur0 <denjy0@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Marius Nechifor <flm.marius@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2025-07-07 17:32:02 -07:00
89 changed files with 1939 additions and 360 deletions
+5 -5
View File
@@ -7,6 +7,7 @@
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty ### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory ### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
### Version V1.0.4 2025-04-05 - kaecyra - Allow user/group to be supplied via CLI, add unattended mode ### Version V1.0.4 2025-04-05 - kaecyra - Allow user/group to be supplied via CLI, add unattended mode
### Version V1.0.5 2025-07-08 - bparkin1283 - use systemctl instead of service for stopping app
### Boilerplate Warning ### Boilerplate Warning
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
@@ -167,11 +168,10 @@ if ! getent group "$app_guid" | grep -qw "$app_uid"; then
echo "Added User [$app_uid] to Group [$app_guid]" echo "Added User [$app_uid] to Group [$app_guid]"
fi fi
# Stop the App if running # Stop and disable the App if running
if service --status-all | grep -Fq "$app"; then if [ $(systemctl is-active "$app") = "active" ]; then
systemctl stop "$app" systemctl disable --now -q "$app"
systemctl disable "$app".service echo "Stopped and disabled existing $app"
echo "Stopped existing $app"
fi fi
# Create Appdata Directory # Create Appdata Directory
@@ -1,4 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
@@ -10,6 +12,8 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { setQueueRemovalOption } from 'Store/Actions/queueActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css'; import styles from './RemoveQueueItemModal.css';
@@ -31,12 +35,6 @@ interface RemoveQueueItemModalProps {
onModalClose: () => void; onModalClose: () => void;
} }
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) { function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const { const {
isOpen, isOpen,
@@ -49,12 +47,13 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
onModalClose, onModalClose,
} = props; } = props;
const dispatch = useDispatch();
const multipleSelected = selectedCount && selectedCount > 1; const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] = const { removalMethod, blocklistMethod } = useSelector(
useState<RemovalMethod>('removeFromClient'); (state: AppState) => state.queue.removalOptions
const [blocklistMethod, setBlocklistMethod] = );
useState<BlocklistMethod>('doNotBlocklist');
const { title, message } = useMemo(() => { const { title, message } = useMemo(() => {
if (!selectedCount) { if (!selectedCount) {
@@ -138,18 +137,11 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
return options; return options;
}, [isPending, multipleSelected]); }, [isPending, multipleSelected]);
const handleRemovalMethodChange = useCallback( const handleRemovalOptionInputChange = useCallback(
({ value }: { value: RemovalMethod }) => { ({ name, value }: InputChanged) => {
setRemovalMethod(value); dispatch(setQueueRemovalOption({ [name]: value }));
}, },
[setRemovalMethod] [dispatch]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
); );
const handleConfirmRemove = useCallback(() => { const handleConfirmRemove = useCallback(() => {
@@ -159,23 +151,11 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
blocklist: blocklistMethod !== 'doNotBlocklist', blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly', skipRedownload: blocklistMethod === 'blocklistOnly',
}); });
}, [removalMethod, blocklistMethod, onRemovePress]);
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
const handleModalClose = useCallback(() => { const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose(); onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]); }, [onModalClose]);
return ( return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}> <Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
@@ -198,7 +178,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
helpTextWarning={translate( helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning' 'RemoveQueueItemRemovalMethodHelpTextWarning'
)} )}
onChange={handleRemovalMethodChange} onChange={handleRemovalOptionInputChange}
/> />
</FormGroup> </FormGroup>
)} )}
@@ -216,7 +196,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
value={blocklistMethod} value={blocklistMethod}
values={blocklistMethodOptions} values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')} helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange} onChange={handleRemovalOptionInputChange}
/> />
</FormGroup> </FormGroup>
</ModalBody> </ModalBody>
+12
View File
@@ -32,6 +32,17 @@ export interface QueuePagedAppState
removeError: Error; removeError: Error;
} }
export type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
export type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
interface RemovalOptions {
removalMethod: RemovalMethod;
blocklistMethod: BlocklistMethod;
}
interface QueueAppState { interface QueueAppState {
status: AppSectionItemState<QueueStatus>; status: AppSectionItemState<QueueStatus>;
details: QueueDetailsAppState; details: QueueDetailsAppState;
@@ -39,6 +50,7 @@ interface QueueAppState {
options: { options: {
includeUnknownSeriesItems: boolean; includeUnknownSeriesItems: boolean;
}; };
removalOptions: RemovalOptions;
} }
export default QueueAppState; export default QueueAppState;
+1 -1
View File
@@ -83,7 +83,7 @@ function RootFolderRow(props: RootFolderRowProps) {
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('RemoveRootFolder')} title={translate('RemoveRootFolder')}
message={translate('RemoveRootFolderMessageText', { path })} message={translate('RemoveRootFolderWithSeriesMessageText', { path })}
confirmLabel={translate('Remove')} confirmLabel={translate('Remove')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -5,7 +5,6 @@
.header { .header {
position: relative; position: relative;
width: 100%; width: 100%;
height: 425px;
} }
.backdrop { .backdrop {
@@ -30,20 +29,18 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
color: var(--white); color: var(--white);
gap: 35px;
} }
.poster { .poster {
flex-shrink: 0; flex-shrink: 0;
margin-right: 35px;
width: 250px; width: 250px;
height: 368px; height: 368px;
} }
.info { .info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden; overflow: hidden;
width: 100%;
} }
.titleRow { .titleRow {
@@ -59,10 +56,13 @@
} }
.title { .title {
overflow: auto;
max-height: calc(3 * 50px);
text-wrap: balance; text-wrap: balance;
font-weight: 300; font-weight: 300;
font-size: 50px; font-size: 50px;
line-height: 50px; line-height: 50px;
line-clamp: 3;
} }
.toggleMonitoredContainer { .toggleMonitoredContainer {
@@ -170,6 +170,8 @@
} }
.title { .title {
overflow: hidden;
max-height: calc(3 * 30px);
font-weight: 300; font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 30px; line-height: 30px;
+1 -17
View File
@@ -1,7 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import TextTruncate from 'react-text-truncate';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
@@ -21,7 +20,6 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import useMeasure from 'Helpers/Hooks/useMeasure';
import usePrevious from 'Helpers/Hooks/usePrevious'; import usePrevious from 'Helpers/Hooks/usePrevious';
import { import {
align, align,
@@ -56,7 +54,6 @@ import {
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions'; import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import fonts from 'Styles/Variables/fonts';
import sortByProp from 'Utilities/Array/sortByProp'; import sortByProp from 'Utilities/Array/sortByProp';
import { findCommand, isCommandExecuting } from 'Utilities/Command'; import { findCommand, isCommandExecuting } from 'Utilities/Command';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -75,9 +72,6 @@ import SeriesProgressLabel from './SeriesProgressLabel';
import SeriesTags from './SeriesTags'; import SeriesTags from './SeriesTags';
import styles from './SeriesDetails.css'; import styles from './SeriesDetails.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images: Image[]) { function getFanartUrl(images: Image[]) {
return images.find((image) => image.coverType === 'fanart')?.url; return images.find((image) => image.coverType === 'fanart')?.url;
} }
@@ -246,7 +240,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
allCollapsed: false, allCollapsed: false,
seasons: {}, seasons: {},
}); });
const [overviewRef, { height: overviewHeight }] = useMeasure();
const wasRefreshing = usePrevious(isRefreshing); const wasRefreshing = usePrevious(isRefreshing);
const wasRenaming = usePrevious(isRenaming); const wasRenaming = usePrevious(isRenaming);
@@ -796,16 +789,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
/> />
</div> </div>
<div ref={overviewRef} className={styles.overview}> <div className={styles.overview}>{overview}</div>
<TextTruncate
line={
Math.floor(
overviewHeight / (defaultFontSize * lineHeight)
) - 1
}
text={overview}
/>
</div>
<MetadataAttribution /> <MetadataAttribution />
</div> </div>
@@ -1,6 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useCallback, useState } from 'react'; import React, { SyntheticEvent, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames'; import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
@@ -122,8 +123,31 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
setIsDeleteSeriesModalOpen(false); setIsDeleteSeriesModalOpen(false);
}, [setIsDeleteSeriesModalOpen]); }, [setIsDeleteSeriesModalOpen]);
const [selectState, selectDispatch] = useSelect();
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
window.open(`/series/${titleSlug}`, '_blank');
return;
}
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',
id: seriesId,
isSelected: !selectState.selectedState[seriesId],
shiftKey,
});
},
[seriesId, selectState.selectedState, selectDispatch, titleSlug]
);
const link = `/series/${titleSlug}`; const link = `/series/${titleSlug}`;
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
height: `${posterHeight}px`, height: `${posterHeight}px`,
@@ -175,7 +199,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
/> />
) : null} ) : null}
<Link className={styles.link} style={elementStyle} to={link}> <Link className={styles.link} style={elementStyle} {...linkProps}>
<SeriesPoster <SeriesPoster
style={elementStyle} style={elementStyle}
images={images} images={images}
@@ -361,6 +361,24 @@ function MediaManagement() {
/> />
</FormGroup> </FormGroup>
) : null} ) : null}
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('UserRejectedExtensions')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="userRejectedExtensions"
helpTexts={[
translate('UserRejectedExtensionsHelpText'),
translate('UserRejectedExtensionsTextsExamples'),
]}
onChange={handleInputChange}
{...settings.userRejectedExtensions}
/>
</FormGroup>
</FieldSet> </FieldSet>
) : null} ) : null}
+4 -4
View File
@@ -55,13 +55,13 @@ function Tag({ id, label }: TagProps) {
}, []); }, []);
const handleConfirmDeleteTag = useCallback(() => { const handleConfirmDeleteTag = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
const handleDeleteTagModalClose = useCallback(() => {
dispatch(deleteTag({ id })); dispatch(deleteTag({ id }));
}, [id, dispatch]); }, [id, dispatch]);
const handleDeleteTagModalClose = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
return ( return (
<Card <Card
className={styles.tag} className={styles.tag}
@@ -31,6 +31,11 @@ export const defaultState = {
includeUnknownSeriesItems: true includeUnknownSeriesItems: true
}, },
removalOptions: {
removalMethod: 'removeFromClient',
blocklistMethod: 'doNotBlocklist'
},
status: { status: {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
@@ -225,6 +230,7 @@ export const defaultState = {
export const persistState = [ export const persistState = [
'queue.options', 'queue.options',
'queue.removalOptions',
'queue.paged.pageSize', 'queue.paged.pageSize',
'queue.paged.sortKey', 'queue.paged.sortKey',
'queue.paged.sortDirection', 'queue.paged.sortDirection',
@@ -257,6 +263,7 @@ export const SET_QUEUE_SORT = 'queue/setQueueSort';
export const SET_QUEUE_FILTER = 'queue/setQueueFilter'; export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption'; export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
export const SET_QUEUE_OPTION = 'queue/setQueueOption'; export const SET_QUEUE_OPTION = 'queue/setQueueOption';
export const SET_QUEUE_REMOVAL_OPTION = 'queue/setQueueRemoveOption';
export const CLEAR_QUEUE = 'queue/clearQueue'; export const CLEAR_QUEUE = 'queue/clearQueue';
export const GRAB_QUEUE_ITEM = 'queue/grabQueueItem'; export const GRAB_QUEUE_ITEM = 'queue/grabQueueItem';
@@ -282,6 +289,7 @@ export const setQueueSort = createThunk(SET_QUEUE_SORT);
export const setQueueFilter = createThunk(SET_QUEUE_FILTER); export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION); export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
export const setQueueOption = createAction(SET_QUEUE_OPTION); export const setQueueOption = createAction(SET_QUEUE_OPTION);
export const setQueueRemovalOption = createAction(SET_QUEUE_REMOVAL_OPTION);
export const clearQueue = createAction(CLEAR_QUEUE); export const clearQueue = createAction(CLEAR_QUEUE);
export const grabQueueItem = createThunk(GRAB_QUEUE_ITEM); export const grabQueueItem = createThunk(GRAB_QUEUE_ITEM);
@@ -529,6 +537,18 @@ export const reducers = createHandleActions({
}; };
}, },
[SET_QUEUE_REMOVAL_OPTION]: function(state, { payload }) {
const queueRemovalOptions = state.removalOptions;
return {
...state,
removalOptions: {
...queueRemovalOptions,
...payload
}
};
},
[CLEAR_QUEUE]: createClearReducer(paged, { [CLEAR_QUEUE]: createClearReducer(paged, {
isFetching: false, isFetching: false,
isPopulated: false, isPopulated: false,
@@ -18,5 +18,6 @@ export default interface MediaManagement {
scriptImportPath: string; scriptImportPath: string;
importExtraFiles: boolean; importExtraFiles: boolean;
extraFileExtensions: string; extraFileExtensions: string;
userRejectedExtensions: string;
enableMediaInfo: boolean; enableMediaInfo: boolean;
} }
@@ -215,6 +215,7 @@ namespace NzbDrone.Common.Instrumentation
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn); c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info); c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal); c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
c.ForLogger("Sonarr.Http.Authentication.ApiKeyAuthenticationHandler").WriteToNil(LogLevel.Info);
}); });
} }
@@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class nzb_su_url_to_nzb_lifeFixture : MigrationTest<nzb_su_url_to_nzb_life>
{
[TestCase("Newznab", "https://api.nzb.su")]
[TestCase("Newznab", "http://api.nzb.su")]
public void should_replace_old_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "Nzb.su",
Implementation = impl,
Settings = new NewznabSettings219
{
BaseUrl = baseUrl,
ApiPath = "/api"
}.ToJson(),
ConfigContract = impl + "Settings",
EnableInteractiveSearch = false
});
});
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl.Replace("su", "life"));
}
[TestCase("Newznab", "https://api.indexer.com")]
public void should_not_replace_different_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "Indexer.com",
Implementation = impl,
Settings = new NewznabSettings219
{
BaseUrl = baseUrl,
ApiPath = "/api"
}.ToJson(),
ConfigContract = impl + "Settings",
EnableInteractiveSearch = false
});
});
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl);
}
}
internal class IndexerDefinition219
{
public int Id { get; set; }
public string Name { get; set; }
public JObject Settings { get; set; }
public int Priority { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public HashSet<int> Tags { get; set; }
public int DownloadClientId { get; set; }
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
}
internal class NewznabSettings219
{
public string BaseUrl { get; set; }
public string ApiPath { get; set; }
}
}
@@ -59,6 +59,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public void should_return_season_time_for_season_packs() public void should_return_season_time_for_season_packs()
{ {
var settings = new TorznabSettings(); var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal;
settings.SeedCriteria.SeasonPackSeedTime = 10; settings.SeedCriteria.SeasonPackSeedTime = 10;
Mocker.GetMock<ICachedIndexerSettingsProvider>() Mocker.GetMock<ICachedIndexerSettingsProvider>()
@@ -85,5 +86,71 @@ namespace NzbDrone.Core.Test.IndexerTests
result.Should().NotBeNull(); result.Should().NotBeNull();
result.SeedTime.Should().Be(TimeSpan.FromMinutes(10)); result.SeedTime.Should().Be(TimeSpan.FromMinutes(10));
} }
[Test]
public void should_return_season_ratio_for_season_packs_when_set()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal;
settings.SeedCriteria.SeedRatio = 1.0;
settings.SeedCriteria.SeasonPackSeedRatio = 10.0;
Mocker.GetMock<ICachedIndexerSettingsProvider>()
.Setup(v => v.GetSettings(It.IsAny<int>()))
.Returns(new CachedIndexerSettings
{
FailDownloads = new HashSet<FailDownloads> { FailDownloads.Executables },
SeedCriteriaSettings = settings.SeedCriteria
});
var result = Subject.GetSeedConfiguration(new RemoteEpisode
{
Release = new ReleaseInfo
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 1
},
ParsedEpisodeInfo = new ParsedEpisodeInfo
{
FullSeason = true
}
});
result.Should().NotBeNull();
result.Ratio.Should().Be(10.0);
}
[Test]
public void should_return_standard_ratio_for_season_packs_when_not_set()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseStandardSeedGoal;
settings.SeedCriteria.SeedRatio = 1.0;
settings.SeedCriteria.SeasonPackSeedRatio = 10.0;
Mocker.GetMock<ICachedIndexerSettingsProvider>()
.Setup(v => v.GetSettings(It.IsAny<int>()))
.Returns(new CachedIndexerSettings
{
FailDownloads = new HashSet<FailDownloads> { FailDownloads.Executables },
SeedCriteriaSettings = settings.SeedCriteria
});
var result = Subject.GetSeedConfiguration(new RemoteEpisode
{
Release = new ReleaseInfo
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 1
},
ParsedEpisodeInfo = new ParsedEpisodeInfo
{
FullSeason = true
}
});
result.Should().NotBeNull();
result.Ratio.Should().Be(1.0);
}
} }
} }
@@ -185,6 +185,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Erai-raws] Series Title! - 01 ~ 10 [1080p][Multiple Subtitle]", "Series Title!", 1, 10)] [TestCase("[Erai-raws] Series Title! - 01 ~ 10 [1080p][Multiple Subtitle]", "Series Title!", 1, 10)]
[TestCase("[Erai-raws] Series-Title! 2 - 01 ~ 10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)] [TestCase("[Erai-raws] Series-Title! 2 - 01 ~ 10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)]
[TestCase("Series_Title_2_[01-05]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 5)] [TestCase("Series_Title_2_[01-05]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 5)]
[TestCase("[Moxie] One Series - The Country (892-916) (BD Remux 1080p AAC FLAC) [Dual Audio]", "One Series - The Country", 892, 916)]
[TestCase("[HatSubs] One Series (1017-1088) (WEB 1080p)", "One Series", 1017, 1088)]
[TestCase("[HatSubs] One Series 1017-1088 (WEB 1080p)", "One Series", 1017, 1088)]
// [TestCase("", "", 1, 2)] // [TestCase("", "", 1, 2)]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber) public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber)
@@ -145,6 +145,7 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Title.the.Series.2009.S01E14.Japanese.HDTV.XviD-LOL")] [TestCase("Title.the.Series.2009.S01E14.Japanese.HDTV.XviD-LOL")]
[TestCase("[Erai-raws] To Be Series - 14 (JA) [1080p CR WEB-DL AVC AAC][MultiSub]")]
public void should_parse_language_japanese(string postTitle) public void should_parse_language_japanese(string postTitle)
{ {
var result = LanguageParser.ParseLanguages(postTitle); var result = LanguageParser.ParseLanguages(postTitle);
@@ -50,17 +50,17 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title S01 REMUX Dual Audio AVC 1080p 8-Bit-ZR-", "ZR")] [TestCase("Series Title S01 REMUX Dual Audio AVC 1080p 8-Bit-ZR-", "ZR")]
public void should_parse_release_group(string title, string expected) public void should_parse_release_group(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
} }
[TestCase("Show.Name.2009.S01.1080p.BluRay.DTS5.1.x264-D-Z0N3", "D-Z0N3")] [TestCase("Show.Name.2009.S01.1080p.BluRay.DTS5.1.x264-D-Z0N3", "D-Z0N3")]
[TestCase("Show.Name.S01E01.1080p.WEB-DL.H264.Fight-BB.mkv", "Fight-BB")] [TestCase("Show.Name.S01E01.1080p.WEB-DL.H264.Fight-BB.mkv", "Fight-BB")]
[TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 5.1 Tigole) [QxR]", "Tigole")] [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 5.1 Tigole) [QxR]", "QxR")]
[TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 afm72) [QxR]", "afm72")] [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 afm72) [QxR]", "QxR")]
[TestCase("Show Name (2021) Season 1 S01 (1080p DSNP WEB-DL x265 HEVC 10bit EAC3 5.1 Silence) [QxR]", "Silence")] [TestCase("Show Name (2021) Season 1 S01 (1080p DSNP WEB-DL x265 HEVC 10bit EAC3 5.1 Silence) [QxR]", "QxR")]
[TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 Panda) [QxR]", "Panda")] [TestCase("Show Name (2021) Season 1 S01 (1080p BluRay x265 HEVC 10bit AAC 2.0 Panda) [QxR]", "QxR")]
[TestCase("Show Name (2020) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 2.0 Ghost) [QxR]", "Ghost")] [TestCase("Show Name (2020) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 2.0 Ghost) [QxR]", "QxR")]
[TestCase("Show Name (2020) Season 1 S01 (1080p WEB-DL x265 HEVC 10bit AC3 5.1 MONOLITH) [QxR]", "MONOLITH")] [TestCase("Show Name (2020) Season 1 S01 (1080p WEB-DL x265 HEVC 10bit AC3 5.1 MONOLITH) [QxR]", "QxR")]
[TestCase("The Show S08E09 The Series.1080p.AMZN.WEB-DL.x265.10bit.EAC3.6.0-Qman[UTR]", "UTR")] [TestCase("The Show S08E09 The Series.1080p.AMZN.WEB-DL.x265.10bit.EAC3.6.0-Qman[UTR]", "UTR")]
[TestCase("The Show S03E07 Fire and Series[1080p x265 10bit S87 Joy]", "Joy")] [TestCase("The Show S03E07 Fire and Series[1080p x265 10bit S87 Joy]", "Joy")]
[TestCase("The Show (2016) - S02E01 - Soul Series #1 (1080p NF WEBRip x265 ImE)", "ImE")] [TestCase("The Show (2016) - S02E01 - Soul Series #1 (1080p NF WEBRip x265 ImE)", "ImE")]
@@ -85,7 +85,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")] [TestCase("Series Title (2012) - S01E01 - Episode 1 (1080p BluRay x265 r00t).mkv", "r00t")]
[TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")] [TestCase("Series Title - S01E01 - Girls Gone Wild Exposed (720p x265 EDGE2020).mkv", "EDGE2020")]
[TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")] [TestCase("Series.Title.S01E02.1080p.BluRay.Remux.AVC.FLAC.2.0-E.N.D", "E.N.D")]
[TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")] [TestCase("Show Name (2016) Season 1 S01 (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "QxR")]
[TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC FLAC 2.0 - KRaLiMaRKo", "KRaLiMaRKo")]
[TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")] [TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")]
[TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")] [TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")]
@@ -95,9 +95,30 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.S01E05.1080p.WEB-DL.DDP5.1.H264-BEN.THE.MEN", "BEN.THE.MEN")] [TestCase("Series.S01E05.1080p.WEB-DL.DDP5.1.H264-BEN.THE.MEN", "BEN.THE.MEN")]
[TestCase("Series (2022) S01 (1080p BluRay x265 SDR DDP 5.1 English - JBENT TAoE)", "TAoE")] [TestCase("Series (2022) S01 (1080p BluRay x265 SDR DDP 5.1 English - JBENT TAoE)", "TAoE")]
[TestCase("Series (2005) S21E12 (1080p AMZN WEB-DL x265 SDR DDP 5.1 English - Goki TAoE)", "TAoE")] [TestCase("Series (2005) S21E12 (1080p AMZN WEB-DL x265 SDR DDP 5.1 English - Goki TAoE)", "TAoE")]
[TestCase("Series (2022) S03E12 (1080p AMZN Webrip x265 10 bit EAC3 5 1 - Ainz)[TAoE]", "TAoE")]
[TestCase("Series Things (2016) S04 Part 1 (1080p Webrip NF x265 10bit EAC3 5 1 - AJJMIN) [TAoE]", "TAoE")]
[TestCase("Series Soup (2024) S01 (1080p NF Webrip x265 10bit EAC3 5 1 Multi - ANONAZ)[TAoE]", "TAoE")]
[TestCase("Series (2022) S01 (1080p NF Webrip x265 10bit EAC3 5 1 Atmos - ArcX)[TAoE]", "TAoE")]
[TestCase("Series - King of Titles (2021) S01 (1080p HMAX Webrip x265 10bit AC3 5 1 - bccornfo) [TAoE]", "TAoE")]
[TestCase("Welcome to Series (2022) S04 (1080p AMZN Webrip x265 10bit EAC3 5 1 - DNU)[TAoE]", "TAoE")]
[TestCase("Series Who (2005) S01 (1080p BDRip x265 10bit AC3 5 1 - DrainedDay)[TAoE]", "TAoE")]
[TestCase("Series Down (2019) (1080p AMZN Webrip x265 10bit EAC3 5 1 - DUHiT)[TAoE]", "TAoE")]
[TestCase("Series (2016) S09 (1080p CRAV Webrip x265 10bit EAC3 5 1 - Erie) [TAoE]", "TAoE")]
[TestCase("Common Series Effects (2025) S01 (1080p AMZN Webrip x265 10bit EAC3 2 0 - Frys) [TAoE]", "TAoE")]
[TestCase("Murderbot (2025) S01 (2160p HDR10 DV Hybrid ATVP Webrip x265 10bit EAC3 5 1 Atmos - Goki)[TAoE]", "TAoE")]
[TestCase("Series In Real Life (2019) S01 REPACK (1080p DSNP Webrip x265 10bit AAC 2 0 - HxD)[TAoE]", "TAoE")]
[TestCase("Series Discovery (2017) S02 (1080p BDRip x265 10bit DTS-HD MA 5 1 - jb2049) [TAoE]", "TAoE")]
[TestCase("Series (2021) S03 (1080p DS4K NF Webrip x265 10bit EAC3 5 1 Atmos English - JBENT)[TAoE]", "TAoE")]
[TestCase("SuSeriespergirl (2015) S04 (1080p BDRip x265 10bit AC3 5 1 - Nostradamus)[TAoE]", "TAoE")]
[TestCase("Series (2019) S02 (4Kto1080p ATVP Webrip x265 10bit AC3 5 1 - r0b0t) [TAoE]", "TAoE")]
[TestCase("v (1970) S01 (2160p AIUS HDR10 DV Hybrid BDRip x265 10bit DTS-HD MA 5 1 - Species180) [TAoE]", "TAoE")]
[TestCase("Series (2024) S02 (1080p ATVP Webrip x265 10bit EAC3 5 1 - TheSickle)[TAoE]", "TAoE")]
[TestCase("Series (2016) S05 Part 02 (1080p NF Webrip x265 10bit EAC3 5 1 - xtrem3x) [TAoE]", "TAoE")]
[TestCase("Series (2013) S01 (1080p BDRip x265 10bit DTS-HD MA 5 1 - WEM)[TAoE]", "TAoE")]
[TestCase("The.Series.1989.S00E65.1080p.DSNP.Webrip.x265.10bit.EAC3.5.1.Goki.TAoE", "TAoE")]
public void should_parse_exception_release_group(string title, string expected) public void should_parse_exception_release_group(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
} }
[Test] [Test]
@@ -115,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests
// [TestCase("", "")] // [TestCase("", "")]
public void should_not_include_language_in_release_group(string title, string expected) public void should_not_include_language_in_release_group(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
} }
[TestCase("Series.Title.S02E04.720p.WEB-DL.AAC2.0.H.264-EVL-RP", "EVL")] [TestCase("Series.Title.S02E04.720p.WEB-DL.AAC2.0.H.264-EVL-RP", "EVL")]
@@ -146,7 +167,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S04E06.Episode.Name.720p.WEB-DL.DD5.1.H.264-HarrHD-RePACKPOST", "HarrHD")] [TestCase("Series.Title.S04E06.Episode.Name.720p.WEB-DL.DD5.1.H.264-HarrHD-RePACKPOST", "HarrHD")]
public void should_not_include_repost_in_release_group(string title, string expected) public void should_not_include_repost_in_release_group(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
} }
[TestCase("[FFF] Series Title!! - S01E11 - Someday, With Sonarr", "FFF")] [TestCase("[FFF] Series Title!! - S01E11 - Someday, With Sonarr", "FFF")]
@@ -159,13 +180,13 @@ namespace NzbDrone.Core.Test.ParserTests
// [TestCase("", "")] // [TestCase("", "")]
public void should_parse_anime_release_groups(string title, string expected) public void should_parse_anime_release_groups(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
} }
[TestCase("Terrible.Anime.Title.001.DBOX.480p.x264-iKaos [v3] [6AFFEF6B]")] [TestCase("Terrible.Anime.Title.001.DBOX.480p.x264-iKaos [v3] [6AFFEF6B]")]
public void should_not_parse_anime_hash_as_release_group(string title) public void should_not_parse_anime_hash_as_release_group(string title)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().BeNull(); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().BeNull();
} }
} }
} }
@@ -36,6 +36,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)] [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)] [TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)]
[TestCase("[Zoombie] Series 100: Bucket List S01 [Web][MKV][h265 10-bit][1080p][AC3 2.0][Softsubs (Zoombie)]", "Series 100: Bucket List", 1)] [TestCase("[Zoombie] Series 100: Bucket List S01 [Web][MKV][h265 10-bit][1080p][AC3 2.0][Softsubs (Zoombie)]", "Series 100: Bucket List", 1)]
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10 bits DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10-bits DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
[TestCase("[GROUP] Series: Title (2023) (Season 1) [BDRip] [1080p Dual Audio HEVC 10-bit DDP] (serie) (Batch)", "Series: Title (2023)", 1)]
[TestCase("Seriesless (2016/S01/WEB-DL/1080p/AC3 5.1/DUAL/SUB)", "Seriesless (2016)", 1)] [TestCase("Seriesless (2016/S01/WEB-DL/1080p/AC3 5.1/DUAL/SUB)", "Seriesless (2016)", 1)]
public void should_parse_full_season_release(string postTitle, string title, int season) public void should_parse_full_season_release(string postTitle, string title, int season)
{ {
@@ -0,0 +1,19 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ParserTests
{
[TestFixture]
public class SubGroupParserFixture : CoreTest
{
[TestCase("[GHOST][1080p] Series - 25 [BD HEVC 10bit Dual Audio AC3][AE0ADDBA]", "GHOST")]
public void should_parse_sub_group_from_title_as_release_group(string title, string expected)
{
var result = Parser.Parser.ParseTitle(title);
result.Should().NotBeNull();
result.ReleaseGroup.Should().Be(expected);
}
}
}
@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.ParserTests
public void should_not_parse_url_in_group(string title, string expected) public void should_not_parse_url_in_group(string title, string expected)
{ {
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
} }
} }
} }
@@ -209,9 +209,20 @@ namespace NzbDrone.Core.Configuration
return AuthenticationType.Forms; return AuthenticationType.Forms;
} }
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue) var value = Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
? enumValue ? enumValue
: GetValueEnum("AuthenticationMethod", AuthenticationType.None); : GetValueEnum("AuthenticationMethod", AuthenticationType.None);
#pragma warning disable CS0618 // Type or member is obsolete
if (value == AuthenticationType.Basic)
#pragma warning restore CS0618 // Type or member is obsolete
{
SetValue("AuthenticationMethod", AuthenticationType.Forms);
return AuthenticationType.Forms;
}
return value;
} }
} }
@@ -386,6 +397,12 @@ namespace NzbDrone.Core.Configuration
{ {
SetValue("EnableSsl", false); SetValue("EnableSsl", false);
} }
#pragma warning disable CS0618 // Type or member is obsolete
if (AuthenticationMethod == AuthenticationType.Basic)
#pragma warning restore CS0618 // Type or member is obsolete
{
SetValue("AuthenticationMethod", AuthenticationType.Forms);
}
} }
private void DeleteOldValues() private void DeleteOldValues()
@@ -257,6 +257,12 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EpisodeTitleRequired", value); } set { SetValue("EpisodeTitleRequired", value); }
} }
public string UserRejectedExtensions
{
get { return GetValue("UserRejectedExtensions", string.Empty); }
set { SetValue("UserRejectedExtensions", value); }
}
public bool SetPermissionsLinux public bool SetPermissionsLinux
{ {
get { return GetValueBoolean("SetPermissionsLinux", false); } get { return GetValueBoolean("SetPermissionsLinux", false); }
@@ -41,6 +41,7 @@ namespace NzbDrone.Core.Configuration
string ExtraFileExtensions { get; set; } string ExtraFileExtensions { get; set; }
RescanAfterRefreshType RescanAfterRefresh { get; set; } RescanAfterRefreshType RescanAfterRefresh { get; set; }
EpisodeTitleRequiredType EpisodeTitleRequired { get; set; } EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
string UserRejectedExtensions { get; set; }
// Permissions (Media Management) // Permissions (Media Management)
bool SetPermissionsLinux { get; set; } bool SetPermissionsLinux { get; set; }
@@ -12,6 +12,7 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.Datastore.Migration namespace NzbDrone.Core.Datastore.Migration
@@ -809,7 +810,7 @@ namespace NzbDrone.Core.Datastore.Migration
private static string GetSceneNameMatch(string sceneName, params string[] tokens) private static string GetSceneNameMatch(string sceneName, params string[] tokens)
{ {
sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty; sceneName = sceneName.IsNotNullOrWhiteSpace() ? FileExtensions.RemoveFileExtension(sceneName) : string.Empty;
foreach (var token in tokens) foreach (var token in tokens)
{ {
@@ -0,0 +1,16 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(219)]
public class nzb_su_url_to_nzb_life : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.Sql("UPDATE \"Indexers\" SET \"Settings\" = replace(\"Settings\", '//api.nzb.su', '//api.nzb.life')" +
"WHERE \"Implementation\" = 'Newznab'" +
"AND \"Settings\" LIKE '%//api.nzb.su%'");
}
}
}
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(229)]
public class enable_season_pack_seeding_goal : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Execute.WithConnection(SetSeasonPackSeedingGoal);
}
private void SetSeasonPackSeedingGoal(IDbConnection conn, IDbTransaction tran)
{
var updatedIndexers = new List<object>();
using var selectCommand = conn.CreateCommand();
selectCommand.Transaction = tran;
selectCommand.CommandText = "SELECT * FROM \"Indexers\"";
using var reader = selectCommand.ExecuteReader();
while (reader.Read())
{
var idIndex = reader.GetOrdinal("Id");
var settingsIndex = reader.GetOrdinal("Settings");
var id = reader.GetInt32(idIndex);
var settings = Json.Deserialize<Dictionary<string, object>>(reader.GetString(settingsIndex));
if (settings.TryGetValue("seedCriteria", out var seedCriteriaToken) && seedCriteriaToken is JObject seedCriteria)
{
if (seedCriteria?["seasonPackSeedTime"] != null)
{
seedCriteria["seasonPackSeedGoal"] = 1;
if (seedCriteria["seedRatio"] != null)
{
seedCriteria["seasonPackSeedRatio"] = seedCriteria["seedRatio"];
}
updatedIndexers.Add(new
{
Settings = settings.ToJson(),
Id = id,
});
}
}
}
if (updatedIndexers.Any())
{
var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
conn.Execute(updateSql, updatedIndexers, transaction: tran);
}
}
}
}
@@ -14,6 +14,7 @@ using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Tags;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.QBittorrent namespace NzbDrone.Core.Download.Clients.QBittorrent
@@ -22,6 +23,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{ {
private readonly IQBittorrentProxySelector _proxySelector; private readonly IQBittorrentProxySelector _proxySelector;
private readonly ICached<SeedingTimeCacheEntry> _seedingTimeCache; private readonly ICached<SeedingTimeCacheEntry> _seedingTimeCache;
private readonly ITagRepository _tagRepository;
private class SeedingTimeCacheEntry private class SeedingTimeCacheEntry
{ {
@@ -38,12 +40,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ICacheManager cacheManager, ICacheManager cacheManager,
ILocalizationService localizationService, ILocalizationService localizationService,
IBlocklistService blocklistService, IBlocklistService blocklistService,
ITagRepository tagRepository,
Logger logger) Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger)
{ {
_proxySelector = proxySelector; _proxySelector = proxySelector;
_seedingTimeCache = cacheManager.GetCache<SeedingTimeCacheEntry>(GetType(), "seedingTime"); _seedingTimeCache = cacheManager.GetCache<SeedingTimeCacheEntry>(GetType(), "seedingTime");
_tagRepository = tagRepository;
} }
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings); private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
@@ -83,7 +87,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? remoteEpisode.SeedConfiguration : null, Settings); Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? remoteEpisode.SeedConfiguration : null, Settings);
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart || (Settings.AddSeriesTags && remoteEpisode.Series.Tags.Count > 0))
{ {
if (!WaitForTorrent(hash)) if (!WaitForTorrent(hash))
{ {
@@ -125,6 +129,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_logger.Warn(ex, "Failed to set ForceStart for {0}.", hash); _logger.Warn(ex, "Failed to set ForceStart for {0}.", hash);
} }
} }
if (Settings.AddSeriesTags && remoteEpisode.Series.Tags.Count > 0)
{
try
{
Proxy.AddTags(hash.ToLower(), _tagRepository.GetTags(remoteEpisode.Series.Tags).Select(tag => tag.Label), Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to add tags for {0}.", hash);
}
}
} }
return hash; return hash;
@@ -140,7 +156,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? remoteEpisode.SeedConfiguration : null, Settings); Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? remoteEpisode.SeedConfiguration : null, Settings);
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart || (Settings.AddSeriesTags && remoteEpisode.Series.Tags.Count > 0))
{ {
if (!WaitForTorrent(hash)) if (!WaitForTorrent(hash))
{ {
@@ -182,6 +198,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_logger.Warn(ex, "Failed to set ForceStart for {0}.", hash); _logger.Warn(ex, "Failed to set ForceStart for {0}.", hash);
} }
} }
if (Settings.AddSeriesTags && remoteEpisode.Series.Tags.Count > 0)
{
try
{
Proxy.AddTags(hash.ToLower(), _tagRepository.GetTags(remoteEpisode.Series.Tags).Select(tag => tag.Label), Settings);
}
catch (Exception ex)
{
_logger.Warn(ex, "Failed to add tags for {0}.", hash);
}
}
} }
return hash; return hash;
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings); void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
void AddTags(string hash, IEnumerable<string> tags, QBittorrentSettings settings);
} }
public interface IQBittorrentProxySelector public interface IQBittorrentProxySelector
@@ -273,6 +273,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ProcessRequest(request, settings); ProcessRequest(request, settings);
} }
public void AddTags(string hash, IEnumerable<string> tags, QBittorrentSettings settings)
{
// Not supported on api v1
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{ {
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
@@ -338,6 +338,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ProcessRequest(request, settings); ProcessRequest(request, settings);
} }
public void AddTags(string hash, IEnumerable<string> tags, QBittorrentSettings settings)
{
var request = BuildRequest(settings).Resource("/api/v2/torrents/addTags")
.Post()
.AddFormParameter("hashes", hash)
.AddFormParameter("tags", string.Join(",", tags));
ProcessRequest(request, settings);
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{ {
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
@@ -73,6 +73,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] [FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; } public int ContentLayout { get; set; }
[FieldDefinition(14, Label = "DownloadClientQbittorrentSettingsAddSeriesTags", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsAddSeriesTagsHelpText")]
public bool AddSeriesTags { get; set; }
public override NzbDroneValidationResult Validate() public override NzbDroneValidationResult Validate()
{ {
return new NzbDroneValidationResult(Validator.Validate(this)); return new NzbDroneValidationResult(Validator.Validate(this));
@@ -0,0 +1,155 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace NzbDrone.Core.Download.Clients.Tribler
{
public enum DownloadStatus
{
[EnumMember(Value = @"WAITING4HASHCHECK")]
Waiting4HashCheck = 0,
[EnumMember(Value = @"HASHCHECKING")]
Hashchecking = 1,
[EnumMember(Value = @"METADATA")]
Metadata = 2,
[EnumMember(Value = @"DOWNLOADING")]
Downloading = 3,
[EnumMember(Value = @"SEEDING")]
Seeding = 4,
[EnumMember(Value = @"STOPPED")]
Stopped = 5,
[EnumMember(Value = @"ALLOCATING_DISKSPACE")]
AllocatingDiskspace = 6,
[EnumMember(Value = @"EXIT_NODES")]
Exitnodes = 7,
[EnumMember(Value = @"CIRCUITS")]
Circuits = 8,
[EnumMember(Value = @"STOPPED_ON_ERROR")]
StoppedOnError = 9,
[EnumMember(Value = @"LOADING")]
Loading = 10,
}
public class Trackers
{
public string Url { get; set; }
[JsonProperty("peers")]
public object Peers { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
}
public class Download
{
public string Name { get; set; }
public float? Progress { get; set; }
public string Infohash { get; set; }
public bool? AnonDownload { get; set; }
public float? Availability { get; set; }
public double? Eta { get; set; }
public long? TotalPieces { get; set; }
public long? NumSeeds { get; set; }
public long? AllTimeUpload { get; set; }
public long? AllTimeDownload { get; set; }
[JsonProperty("status")]
[JsonConverter(typeof(StringEnumConverter))]
public DownloadStatus? Status { get; set; }
public int? StatusCode { get; set; }
public float? AllTimeRatio { get; set; }
public long? TimeAdded { get; set; }
public long? MaxUploadSpeed { get; set; }
public long? MaxDownloadSpeed { get; set; }
public long? Hops { get; set; }
public bool? SafeSeeding { get; set; }
public string Error { get; set; }
public long? TotalDown { get; set; }
public long? Size { get; set; }
public string Destination { get; set; }
public float? SpeedDown { get; set; }
public float? SpeedUp { get; set; }
public long? NumPeers { get; set; }
public List<Trackers> Trackers { get; set; }
}
public class DownloadsResponse
{
public List<Download> Downloads { get; set; }
}
public class AddDownloadRequest
{
[JsonProperty("anon_hops")]
public long? AnonymityHops { get; set; }
[JsonProperty("safe_seeding")]
public bool? SafeSeeding { get; set; }
public string Destination { get; set; }
[JsonProperty("uri", Required = Newtonsoft.Json.Required.Always)]
[Required(AllowEmptyStrings = true)]
public string Uri { get; set; }
}
public class AddDownloadResponse
{
public string Infohash { get; set; }
public bool? Started { get; set; }
}
public class RemoveDownloadRequest
{
[JsonProperty("remove_data")]
public bool? RemoveData { get; set; }
}
public class DeleteDownloadResponse
{
public bool? Removed { get; set; }
public string Infohash { get; set; }
}
public class UpdateDownloadRequest
{
[JsonProperty("anon_hops")]
public long? AnonHops { get; set; }
[JsonProperty("selected_files")]
public List<int> Selected_files { get; set; }
public string State { get; set; }
}
public class UpdateDownloadResponse
{
public bool? Modified { get; set; }
public string Infohash { get; set; }
}
public class File
{
public long? Size { get; set; }
public long? Index { get; set; }
public string Name { get; set; }
public float? Progress { get; set; }
public bool? Included { get; set; }
}
public class GetFilesResponse
{
public List<File> Files { get; set; }
}
}
@@ -0,0 +1,189 @@
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace NzbDrone.Core.Indexers.Tribler
{
public class TriblerSettingsResponse
{
public Settings Settings { get; set; }
}
public class Settings
{
public Api Api { get; set; }
public bool Statistics { get; set; }
[JsonProperty("content_discovery_community")]
public ContentDiscoveryCommunity ContentDiscoveryCommunity { get; set; }
public Database Database { get; set; }
[JsonProperty("dht_discovery")]
public DHTDiscovery DHTDiscovery { get; set; }
[JsonProperty("knowledge_community")]
public KnowledgeCommunity KnowledgeCommunity { get; set; }
public LibTorrent LibTorrent { get; set; }
public Recommender Recommender { get; set; }
public Rendezvous RecoRendezvousmmender { get; set; }
[JsonProperty("torrent_checker")]
public TorrentChecker TorrentChecker { get; set; }
[JsonProperty("tunnel_community")]
public TunnelCommunity TunnelCommunity { get; set; }
public Versioning Versioning { get; set; }
[JsonProperty("watch_folder")]
public WatchFolder WatchFolder { get; set; }
[JsonProperty("state_dir")]
public string StateDir { get; set; }
[JsonProperty("memory_db")]
public bool? MemoryDB { get; set; }
}
public class Api
{
[JsonProperty("http_enabled")]
public bool HttpEnabled { get; set; }
[JsonProperty("http_port")]
public int HttpPort { get; set; }
[JsonProperty("http_host")]
public string HttpHost { get; set; }
[JsonProperty("https_enabled")]
public bool HttpsEnabled { get; set; }
[JsonProperty("https_port")]
public int HttpsPort { get; set; }
[JsonProperty("https_host")]
public string HttpsHost { get; set; }
[JsonProperty("https_certfile")]
public string HttpsCertFile { get; set; }
[JsonProperty("http_port_running")]
public int HttpPortRunning { get; set; }
[JsonProperty("https_port_running")]
public int HttpsPortRunning { get; set; }
}
public class ContentDiscoveryCommunity
{
public bool? Enabled { get; set; }
}
public class Database
{
public bool? Enabled { get; set; }
}
public class DHTDiscovery
{
public bool? Enabled { get; set; }
}
public class KnowledgeCommunity
{
public bool? Enabled { get; set; }
}
public class LibTorrent
{
[JsonProperty("download_defaults")]
public LibTorrentDownloadDefaults DownloadDefaults { get; set; }
// contains a lot more data, but it's not needed currently
}
public class Recommender
{
public bool? Enabled { get; set; }
}
public class Rendezvous
{
public bool? Enabled { get; set; }
}
public class TorrentChecker
{
[JsonProperty("enabled")]
public bool? Enabled { get; set; }
}
public class TunnelCommunity
{
[JsonProperty("enabled")]
public bool? Enabled { get; set; }
[JsonProperty("min_circuits")]
public int? MinCircuits { get; set; }
[JsonProperty("max_circuits")]
public int? MaxCircuits { get; set; }
}
public class Versioning
{
[JsonProperty("enabled")]
public bool? Enabled { get; set; }
}
public class WatchFolder
{
[JsonProperty("enabled")]
public bool? Enabled { get; set; }
[JsonProperty("directory")]
public string Directory { get; set; }
[JsonProperty("check_interval")]
public int? CheckInterval { get; set; }
}
public class LibTorrentDownloadDefaults
{
[JsonProperty("anonymity_enabled")]
public bool? AnonymityEnabled { get; set; }
[JsonProperty("number_hops")]
public int? NumberHops { get; set; }
[JsonProperty("safeseeding_enabled")]
public bool? SafeSeedingEnabled { get; set; }
[JsonProperty("saveas")]
public string SaveAS { get; set; }
[JsonProperty("seeding_mode")]
[JsonConverter(typeof(StringEnumConverter))]
public DownloadDefaultsSeedingMode? SeedingMode { get; set; }
[JsonProperty("seeding_ratio")]
public double? SeedingRatio { get; set; }
[JsonProperty("seeding_time")]
public double? SeedingTime { get; set; }
}
public enum DownloadDefaultsSeedingMode
{
[EnumMember(Value = @"ratio")]
Ratio = 0,
[EnumMember(Value = @"forever")]
Forever = 1,
[EnumMember(Value = @"time")]
Time = 2,
[EnumMember(Value = @"never")]
Never = 3,
}
}
@@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers.Tribler;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Tribler
{
public class TriblerDownloadClient : TorrentClientBase<TriblerDownloadSettings>
{
private readonly ITriblerDownloadClientProxy _proxy;
public TriblerDownloadClient(
ITriblerDownloadClientProxy triblerDownloadClientProxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
ILocalizationService localizationService,
IBlocklistService blocklistService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger)
{
_proxy = triblerDownloadClientProxy;
}
public override string Name => "Tribler";
public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientTriblerProviderMessage", new Dictionary<string, object> { { "clientName", Name }, { "clientVersionRange", "8.0.7" } }), ProviderMessageType.Warning);
public override bool PreferTorrentFile => false;
public override IEnumerable<DownloadClientItem> GetItems()
{
var configAsync = _proxy.GetConfig(Settings);
var items = new List<DownloadClientItem>();
var downloads = _proxy.GetDownloads(Settings);
foreach (var download in downloads)
{
// If totalsize == 0 the torrent is a magnet downloading metadata
if (download.Size == null || download.Size == 0)
{
continue;
}
var item = new DownloadClientItem
{
DownloadId = download.Infohash,
Title = download.Name,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false)
};
// some concurrency could make this faster.
var files = _proxy.GetDownloadFiles(Settings, download);
item.OutputPath = new OsPath(download.Destination);
if (files.Count == 1)
{
item.OutputPath += files.First().Name;
}
else
{
item.OutputPath += item.Title;
}
item.TotalSize = (long)download.Size;
item.RemainingSize = (long)(download.Size * (1 - download.Progress));
item.SeedRatio = download.AllTimeRatio;
if (download.Eta.HasValue)
{
if (download.Eta.Value >= TimeSpan.FromDays(365).TotalSeconds)
{
item.RemainingTime = TimeSpan.FromDays(365);
}
else if (download.Eta.Value < 0)
{
item.RemainingTime = TimeSpan.FromSeconds(0);
}
else
{
item.RemainingTime = TimeSpan.FromSeconds(download.Eta.Value);
}
}
item.Message = download.Error;
// tribler always saves files unencrypted to disk.
item.IsEncrypted = false;
switch (download.Status)
{
case DownloadStatus.Hashchecking:
case DownloadStatus.Waiting4HashCheck:
case DownloadStatus.Circuits:
case DownloadStatus.Exitnodes:
case DownloadStatus.Downloading:
item.Status = DownloadItemStatus.Downloading;
break;
case DownloadStatus.Metadata:
case DownloadStatus.AllocatingDiskspace:
item.Status = DownloadItemStatus.Queued;
break;
case DownloadStatus.Seeding:
case DownloadStatus.Stopped:
item.Status = DownloadItemStatus.Completed;
break;
case DownloadStatus.StoppedOnError:
item.Status = DownloadItemStatus.Failed;
break;
case DownloadStatus.Loading:
default: // new status in API? default to downloading
item.Message = "Unknown download state: " + download.Status;
_logger.Info(item.Message);
item.Status = DownloadItemStatus.Downloading;
break;
}
// Override status if completed, but not finished downloading
if (download.Status == DownloadStatus.Stopped && download.Progress < 1)
{
item.Status = DownloadItemStatus.Paused;
}
if (download.Error != null && download.Error.Length > 0)
{
item.Status = DownloadItemStatus.Warning;
item.Message = download.Error;
}
item.CanBeRemoved = item.CanMoveFiles = HasReachedSeedLimit(download, configAsync);
items.Add(item);
}
return items;
}
public override void RemoveItem(DownloadClientItem item, bool deleteData)
{
_proxy.RemoveDownload(Settings, item, deleteData);
}
public override DownloadClientInfo GetStatus()
{
var config = _proxy.GetConfig(Settings);
var destDir = config.Settings.LibTorrent.DownloadDefaults.SaveAS;
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
destDir = string.Format("{0}/.{1}", destDir, Settings.TvCategory);
}
return new DownloadClientInfo
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) }
};
}
protected static bool HasReachedSeedLimit(Download torrent, TriblerSettingsResponse config)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
if (torrent == null)
{
throw new ArgumentNullException(nameof(torrent));
}
// if download is still running then it's not finished.
if (torrent.Status != DownloadStatus.Stopped)
{
return false;
}
switch (config.Settings.LibTorrent.DownloadDefaults.SeedingMode)
{
case DownloadDefaultsSeedingMode.Ratio:
return torrent.AllTimeRatio.HasValue
&& torrent.AllTimeRatio >= config.Settings.LibTorrent.DownloadDefaults.SeedingRatio;
case DownloadDefaultsSeedingMode.Time:
var downloadStarted = DateTimeOffset.FromUnixTimeSeconds(torrent.TimeAdded.Value);
var maxSeedingTime = TimeSpan.FromSeconds(config.Settings.LibTorrent.DownloadDefaults.SeedingTime ?? 0);
return torrent.TimeAdded.HasValue
&& downloadStarted.Add(maxSeedingTime) < DateTimeOffset.Now;
case DownloadDefaultsSeedingMode.Never:
return true;
case DownloadDefaultsSeedingMode.Forever:
default:
return false;
}
}
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
var addDownloadRequestObject = new AddDownloadRequest
{
Destination = GetDownloadDirectory(),
Uri = magnetLink,
SafeSeeding = Settings.SafeSeeding,
AnonymityHops = Settings.AnonymityLevel
};
return _proxy.AddFromMagnetLink(Settings, addDownloadRequestObject);
}
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
{
// TODO: Tribler 8.x does support adding from a torrent file, but it's not a simple put command.
throw new NotSupportedException("Tribler does not support torrent files, only magnet links");
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.HasErrors())
{
return;
}
}
protected string GetDownloadDirectory()
{
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
{
return Settings.TvDirectory;
}
if (!Settings.TvCategory.IsNotNullOrWhiteSpace())
{
return null;
}
var config = _proxy.GetConfig(Settings);
var destDir = config.Settings.LibTorrent.DownloadDefaults.SaveAS;
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
}
protected ValidationFailure TestConnection()
{
try
{
var downloads = GetItems();
return null;
}
catch (DownloadClientAuthenticationException ex)
{
_logger.Error(ex, ex.Message);
return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("DownloadClientValidationApiKeyIncorrect"));
}
catch (DownloadClientUnavailableException ex)
{
_logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary<string, object> { { "clientName", Name } }))
{
DetailedDescription = ex.Message
};
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to test");
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary<string, object> { { "exception", ex.Message } }));
}
}
}
}
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Indexers.Tribler;
namespace NzbDrone.Core.Download.Clients.Tribler
{
public interface ITriblerDownloadClientProxy
{
List<Download> GetDownloads(TriblerDownloadSettings settings);
List<File> GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem);
TriblerSettingsResponse GetConfig(TriblerDownloadSettings settings);
void RemoveDownload(TriblerDownloadSettings settings, DownloadClientItem item, bool deleteData);
string AddFromMagnetLink(TriblerDownloadSettings settings, AddDownloadRequest downloadRequest);
}
public class TriblerDownloadClientProxy : ITriblerDownloadClientProxy
{
protected readonly IHttpClient _httpClient;
private readonly Logger _logger;
public TriblerDownloadClientProxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
private HttpRequestBuilder GetRequestBuilder(TriblerDownloadSettings settings, string relativePath = null)
{
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
var requestBuilder = new HttpRequestBuilder(baseUrl)
.Accept(HttpAccept.Json);
requestBuilder.Headers.Add("X-Api-Key", settings.ApiKey);
requestBuilder.LogResponseContent = true;
return requestBuilder;
}
private T ProcessRequest<T>(HttpRequestBuilder requestBuilder)
where T : new()
{
return ProcessRequest<T>(requestBuilder.Build());
}
private T ProcessRequest<T>(HttpRequest requestBuilder)
where T : new()
{
var httpRequest = requestBuilder;
_logger.Debug("Url: {0}", httpRequest.Url);
try
{
var response = _httpClient.Execute(httpRequest);
return Json.Deserialize<T>(response.Content);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
throw new DownloadClientAuthenticationException("Unauthorized - AuthToken is invalid", ex);
}
throw new DownloadClientUnavailableException("Unable to connect to Tribler. Status Code: {0}", ex.Response.StatusCode, ex);
}
}
public TriblerSettingsResponse GetConfig(TriblerDownloadSettings settings)
{
var configRequest = GetRequestBuilder(settings, "api/settings");
return ProcessRequest<TriblerSettingsResponse>(configRequest);
}
public List<File> GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem)
{
var filesRequest = GetRequestBuilder(settings, "api/downloads/" + downloadItem.Infohash + "/files");
return ProcessRequest<GetFilesResponse>(filesRequest).Files;
}
public List<Download> GetDownloads(TriblerDownloadSettings settings)
{
var downloadRequest = GetRequestBuilder(settings, "api/downloads");
var downloads = ProcessRequest<DownloadsResponse>(downloadRequest);
return downloads.Downloads;
}
public void RemoveDownload(TriblerDownloadSettings settings, DownloadClientItem item, bool deleteData)
{
var deleteDownloadRequestObject = new RemoveDownloadRequest
{
RemoveData = deleteData
};
var deleteRequestBuilder = GetRequestBuilder(settings, "api/downloads/" + item.DownloadId.ToLower());
deleteRequestBuilder.Method = HttpMethod.Delete;
var deleteRequest = deleteRequestBuilder.Build();
deleteRequest.SetContent(Json.ToJson(deleteDownloadRequestObject));
ProcessRequest<DeleteDownloadResponse>(deleteRequest);
}
public string AddFromMagnetLink(TriblerDownloadSettings settings, AddDownloadRequest downloadRequest)
{
var addDownloadRequestBuilder = GetRequestBuilder(settings, "api/downloads");
addDownloadRequestBuilder.Method = HttpMethod.Put;
var addDownloadRequest = addDownloadRequestBuilder.Build();
addDownloadRequest.SetContent(Json.ToJson(downloadRequest));
return ProcessRequest<AddDownloadResponse>(addDownloadRequest).Infohash;
}
}
}
@@ -0,0 +1,74 @@
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Tribler
{
public class TriblerSettingsValidator : AbstractValidator<TriblerDownloadSettings>
{
public TriblerSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.UrlBase).ValidUrlBase();
RuleFor(c => c.ApiKey).NotEmpty();
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
RuleFor(c => c.TvCategory).Empty()
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
.WithMessage("Cannot use Category and Directory");
RuleFor(c => c.AnonymityLevel).GreaterThanOrEqualTo(0);
}
}
public class TriblerDownloadSettings : IProviderConfig
{
private static readonly TriblerSettingsValidator Validator = new TriblerSettingsValidator();
public TriblerDownloadSettings()
{
Host = "localhost";
Port = 20100;
UrlBase = "";
AnonymityLevel = 1;
SafeSeeding = true;
}
[FieldDefinition(1, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; }
[FieldDefinition(2, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")]
public bool UseSsl { get; set; }
[FieldDefinition(4, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")]
[FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Tribler")]
[FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]")]
public string UrlBase { get; set; }
[FieldDefinition(5, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, HelpText = "DownloadClientTriblerSettingsApiKeyHelpText")]
public string ApiKey { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")]
public string TvCategory { get; set; }
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTriblerSettingsDirectoryHelpText")]
public string TvDirectory { get; set; }
[FieldDefinition(8, Label = "DownloadClientTriblerSettingsAnonymityLevel", Type = FieldType.Number, HelpText = "DownloadClientTriblerSettingsAnonymityLevelHelpText")]
[FieldToken(TokenField.HelpText, "DownloadClientTriblerSettingsAnonymityLevel", "url", "https://www.tribler.org/anonymity.html")]
public int AnonymityLevel { get; set; }
[FieldDefinition(9, Label = "DownloadClientTriblerSettingsSafeSeeding", Type = FieldType.Checkbox, HelpText = "DownloadClientTriblerSettingsSafeSeedingHelpText")]
public bool SafeSeeding { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -50,6 +50,12 @@ public class RejectedImportService : IRejectedImportService
_logger.Trace("Download '{0}' contains executable file, marking as failed", trackedDownload.DownloadItem.Title); _logger.Trace("Download '{0}' contains executable file, marking as failed", trackedDownload.DownloadItem.Title);
trackedDownload.Fail(); trackedDownload.Fail();
} }
else if (rejectionReason == ImportRejectionReason.UserRejectedExtension &&
indexerSettings.FailDownloads.Contains(FailDownloads.UserDefinedExtensions))
{
_logger.Trace("Download '{0}' contains user defined rejected file extension, marking as failed", trackedDownload.DownloadItem.Title);
trackedDownload.Fail();
}
else else
{ {
trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors)); trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors));
@@ -221,7 +221,7 @@ namespace NzbDrone.Core.Download
try try
{ {
hash = MagnetLink.Parse(magnetUrl).InfoHash.ToHex(); hash = MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
} }
catch (FormatException ex) catch (FormatException ex)
{ {
+4 -1
View File
@@ -8,5 +8,8 @@ public enum FailDownloads
Executables = 0, Executables = 0,
[FieldOption(Label = "Potentially Dangerous")] [FieldOption(Label = "Potentially Dangerous")]
PotentiallyDangerous = 1 PotentiallyDangerous = 1,
[FieldOption(Label = "User Defined Extensions")]
UserDefinedExtensions = 2
} }
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr")); yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com")); yield return GetDefinition("DrunkenSlug", GetSettings("https://drunkenslug.com"));
yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su")); yield return GetDefinition("Nzb.life", GetSettings("https://api.nzb.life"));
yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat")); yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat"));
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws", categories: new[] { 5030, 5040, 5045 })); yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws", categories: new[] { 5030, 5040, 5045 }));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info")); yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));
@@ -13,10 +13,10 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
public class NewznabSettingsValidator : AbstractValidator<NewznabSettings> public class NewznabSettingsValidator : AbstractValidator<NewznabSettings>
{ {
private static readonly string[] ApiKeyWhiteList = private static readonly string[] ApiKeyAllowList =
{ {
"nzbs.org", "nzbs.org",
"nzb.su", "nzb.life",
"dognzb.cr", "dognzb.cr",
"nzbplanet.net", "nzbplanet.net",
"nzbid.org", "nzbid.org",
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Indexers.Newznab
private static bool ShouldHaveApiKey(NewznabSettings settings) private static bool ShouldHaveApiKey(NewznabSettings settings)
{ {
return settings.BaseUrl != null && ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c)); return settings.BaseUrl != null && ApiKeyAllowList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
} }
private static readonly Regex AdditionalParametersRegex = new(@"(&.+?\=.+?)+", RegexOptions.Compiled); private static readonly Regex AdditionalParametersRegex = new(@"(&.+?\=.+?)+", RegexOptions.Compiled);
@@ -0,0 +1,11 @@
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.Indexers;
public enum SeasonPackSeedGoal
{
[FieldOption(Label = "IndexerSettingsSeasonPackSeedGoalUseStandardGoals")]
UseStandardSeedGoal = 0,
[FieldOption(Label = "IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals")]
UseSeasonPackSeedGoal = 1
}
@@ -49,12 +49,16 @@ namespace NzbDrone.Core.Indexers
return null; return null;
} }
var useSeasonPackSeedGoal = (SeasonPackSeedGoal)seedCriteria.SeasonPackSeedGoal == SeasonPackSeedGoal.UseSeasonPackSeedGoal;
var seedConfig = new TorrentSeedConfiguration var seedConfig = new TorrentSeedConfiguration
{ {
Ratio = seedCriteria.SeedRatio Ratio = (fullSeason && useSeasonPackSeedGoal)
? seedCriteria.SeasonPackSeedRatio
: seedCriteria.SeedRatio
}; };
var seedTime = fullSeason ? seedCriteria.SeasonPackSeedTime : seedCriteria.SeedTime; var seedTime = (fullSeason && useSeasonPackSeedGoal) ? seedCriteria.SeasonPackSeedTime : seedCriteria.SeedTime;
if (seedTime.HasValue) if (seedTime.HasValue)
{ {
seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value); seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value);
@@ -17,6 +17,10 @@ namespace NzbDrone.Core.Indexers
.When(c => c.SeedTime.HasValue) .When(c => c.SeedTime.HasValue)
.AsWarning().WithMessage("Should be greater than zero"); .AsWarning().WithMessage("Should be greater than zero");
RuleFor(c => c.SeasonPackSeedRatio).GreaterThan(0.0)
.When(c => c.SeasonPackSeedRatio.HasValue)
.AsWarning().WithMessage("Should be greater than zero");
RuleFor(c => c.SeasonPackSeedTime).GreaterThan(0) RuleFor(c => c.SeasonPackSeedTime).GreaterThan(0)
.When(c => c.SeasonPackSeedTime.HasValue) .When(c => c.SeasonPackSeedTime.HasValue)
.AsWarning().WithMessage("Should be greater than zero"); .AsWarning().WithMessage("Should be greater than zero");
@@ -27,6 +31,11 @@ namespace NzbDrone.Core.Indexers
.When(c => c.SeedRatio > 0.0) .When(c => c.SeedRatio > 0.0)
.AsWarning() .AsWarning()
.WithMessage($"Under {seedRatioMinimum} leads to H&R"); .WithMessage($"Under {seedRatioMinimum} leads to H&R");
RuleFor(c => c.SeasonPackSeedRatio).GreaterThanOrEqualTo(seedRatioMinimum)
.When(c => c.SeasonPackSeedRatio > 0.0)
.AsWarning()
.WithMessage($"Under {seedRatioMinimum} leads to H&R");
} }
if (seedTimeMinimum != 0) if (seedTimeMinimum != 0)
@@ -55,7 +64,13 @@ namespace NzbDrone.Core.Indexers
[FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)] [FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)]
public int? SeedTime { get; set; } public int? SeedTime { get; set; }
[FieldDefinition(2, Type = FieldType.Number, Label = "Season-Pack Seed Time", Unit = "minutes", HelpText = "IndexerSettingsSeasonPackSeedTimeHelpText", Advanced = true)] [FieldDefinition(2, Type = FieldType.Select, Label = "IndexerSettingsSeasonPackSeedGoal", SelectOptions = typeof(SeasonPackSeedGoal), HelpText = "IndexerSettingsSeasonPackSeedGoalHelpText", Advanced = true)]
public int SeasonPackSeedGoal { get; set; }
[FieldDefinition(3, Type = FieldType.Number, Label = "IndexerSettingsSeasonPackSeedRatio", HelpText = "IndexerSettingsSeasonPackSeedRatioHelpText", Advanced = true)]
public double? SeasonPackSeedRatio { get; set; }
[FieldDefinition(4, Type = FieldType.Number, Label = "IndexerSettingsSeasonPackSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeasonPackSeedTimeHelpText", Advanced = true)]
public int? SeasonPackSeedTime { get; set; } public int? SeasonPackSeedTime { get; set; }
} }
} }
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Indexers
{ {
try try
{ {
return MagnetLink.Parse(magnetUrl).InfoHash.ToHex(); return MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
} }
catch catch
{ {
@@ -12,11 +12,11 @@ namespace NzbDrone.Core.Indexers.Torznab
{ {
public class TorznabSettingsValidator : AbstractValidator<TorznabSettings> public class TorznabSettingsValidator : AbstractValidator<TorznabSettings>
{ {
private static readonly string[] ApiKeyWhiteList = Array.Empty<string>(); private static readonly string[] ApiKeyAllowList = Array.Empty<string>();
private static bool ShouldHaveApiKey(TorznabSettings settings) private static bool ShouldHaveApiKey(TorznabSettings settings)
{ {
return settings.BaseUrl != null && ApiKeyWhiteList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c)); return settings.BaseUrl != null && ApiKeyAllowList.Any(c => settings.BaseUrl.ToLowerInvariant().Contains(c));
} }
private static readonly Regex AdditionalParametersRegex = new(@"(&.+?\=.+?)+", RegexOptions.Compiled); private static readonly Regex AdditionalParametersRegex = new(@"(&.+?\=.+?)+", RegexOptions.Compiled);
+12 -2
View File
@@ -776,7 +776,7 @@
"DownloadClientFreeboxSettingsApiUrl": "URL de l'API", "DownloadClientFreeboxSettingsApiUrl": "URL de l'API",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desactiva l'ordenació de TV", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desactiva l'ordenació de TV",
"MonitorNoEpisodes": "Cap", "MonitorNoEpisodes": "Cap",
"RemoveRootFolder": "Elimina la carpeta arrel", "RemoveRootFolder": "Elimina la Carpeta Arrel",
"CustomFormatsSpecificationMaximumSize": "Mida màxima", "CustomFormatsSpecificationMaximumSize": "Mida màxima",
"DownloadClientFloodSettingsAdditionalTags": "Etiquetes addicionals", "DownloadClientFloodSettingsAdditionalTags": "Etiquetes addicionals",
"DownloadClientSettings": "Configuració del client de baixada", "DownloadClientSettings": "Configuració del client de baixada",
@@ -2161,5 +2161,15 @@
"DownloadClientItemErrorMessage": "{clientName} está reportant un error: {message}", "DownloadClientItemErrorMessage": "{clientName} está reportant un error: {message}",
"EpisodesInSeason": "{episodeCount} episodis en la temporada", "EpisodesInSeason": "{episodeCount} episodis en la temporada",
"NotificationsPushcutSettingsNotificationName": "Nom de la notificació", "NotificationsPushcutSettingsNotificationName": "Nom de la notificació",
"AutoTaggingSpecificationNetwork": "Xarxa(es)" "AutoTaggingSpecificationNetwork": "Xarxa(es)",
"MonitorEpisodes": "Monitorar episodis",
"NotificationsAppriseSettingsIncludePosterHelpText": "Inclou el cartell al missatge",
"MonitorEpisodesModalInfo": "Aquesta opció només ajustarà quins episodis o temporades són monitorats en les sèries. Seleccionar Cap deixarà de monitorar les sèries",
"EpisodeMonitoring": "Monitoratge d'episodis",
"NotificationsAppriseSettingsIncludePoster": "Inclou el cartell",
"UserRejectedExtensions": "Extensions addicionals d'arxiu rebutjades",
"UserRejectedExtensionsHelpText": "Llista d'extensions d'arxiu a fallar separades per coma (Descàrregues fallides també necessita ser activat per indexador)",
"UserRejectedExtensionsTextsExamples": "Exemples: '.ext, .xyz' o 'ext,xyz'",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Afegeix etiquetes de sèries",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Afegeix etiquetes de sèries als nous torrents afegits al client de descàrrega (qBittorrent 4.1.0+)"
} }
+21 -4
View File
@@ -481,6 +481,8 @@
"DownloadClientPneumaticSettingsStrmFolder": "Strm Folder", "DownloadClientPneumaticSettingsStrmFolder": "Strm Folder",
"DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone", "DownloadClientPneumaticSettingsStrmFolderHelpText": ".strm files in this folder will be import by drone",
"DownloadClientPriorityHelpText": "Download Client Priority from 1 (Highest) to 50 (Lowest). Default: 1. Round-Robin is used for clients with the same priority.", "DownloadClientPriorityHelpText": "Download Client Priority from 1 (Highest) to 50 (Lowest). Default: 1. Round-Robin is used for clients with the same priority.",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Add Series Tags",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Add series tags to new torrents added to the download client (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsContentLayout": "Content Layout", "DownloadClientQbittorrentSettingsContentLayout": "Content Layout",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Whether to use qBittorrent's configured content layout, the original layout from the torrent or always create a subfolder (qBittorrent 4.3.2+)", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Whether to use qBittorrent's configured content layout, the original layout from the torrent or always create a subfolder (qBittorrent 4.3.2+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First", "DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First",
@@ -547,7 +549,13 @@
"DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}", "DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location", "DownloadClientTransmissionSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Transmission location",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'", "DownloadClientTransmissionSettingsUrlBaseHelpText": "Adds a prefix to the {clientName} rpc url, eg {url}, defaults to '{defaultUrl}'",
"DownloadClientUTorrentProviderMessage": "uTorrent has a history of including cryptominers, malware and ads, we strongly encourage you to choose a different client.", "DownloadClientTriblerSettingsAnonymityLevel": "Anonymity level",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Number of proxies to use when downloading content. To disable set to 0. Proxies reduce download/upload speed. See {url}",
"DownloadClientTriblerSettingsApiKeyHelpText": "[api].key from triblerd.conf",
"DownloadClientTriblerSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Tribler location",
"DownloadClientTriblerSettingsSafeSeeding": "Safe Seeding",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "When enabled, only seed through proxies.",
"DownloadClientTriblerProviderMessage": "The tribler integration is highly experimental. Tested against {clientName} version {clientVersionRange}.",
"DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error", "DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error",
"DownloadClientUnavailable": "Download Client Unavailable", "DownloadClientUnavailable": "Download Client Unavailable",
"DownloadClientValidationApiKeyIncorrect": "API Key Incorrect", "DownloadClientValidationApiKeyIncorrect": "API Key Incorrect",
@@ -1017,8 +1025,14 @@
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.",
"IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrl": "RSS URL",
"IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed", "IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed",
"IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time", "IndexerSettingsSeasonPackSeedGoal": "Seeding Goal for Season Packs",
"IndexerSettingsSeasonPackSeedTimeHelpText": "The time a season-pack torrent should be seeded before stopping, empty uses the download client's default", "IndexerSettingsSeasonPackSeedGoalHelpText": "Choose whether to use different seeding goals for season packs",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Use Standard Goals",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Use Season Pack Goals",
"IndexerSettingsSeasonPackSeedRatio": "Season Pack Seed Ratio",
"IndexerSettingsSeasonPackSeedRatioHelpText": "The ratio a season pack torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules",
"IndexerSettingsSeasonPackSeedTime": "Season Pack Seed Time",
"IndexerSettingsSeasonPackSeedTimeHelpText": "The time a season pack torrent should be seeded before stopping, empty uses the download client's default",
"IndexerSettingsSeedRatio": "Seed Ratio", "IndexerSettingsSeedRatio": "Seed Ratio",
"IndexerSettingsSeedRatioHelpText": "The ratio a torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules", "IndexerSettingsSeedRatioHelpText": "The ratio a torrent should reach before stopping, empty uses the download client's default. Ratio should be at least 1.0 and follow the indexers rules",
"IndexerSettingsSeedTime": "Seed Time", "IndexerSettingsSeedTime": "Seed Time",
@@ -1739,7 +1753,7 @@
"RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.", "RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.",
"RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the downloads and the files from the download client.", "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the downloads and the files from the download client.",
"RemoveRootFolder": "Remove Root Folder", "RemoveRootFolder": "Remove Root Folder",
"RemoveRootFolderMessageText": "Are you sure you want to remove the root folder '{path}'? Files and folders will not be deleted from disk, and series in this root folder will not be removed from {appName}.", "RemoveRootFolderWithSeriesMessageText": "Are you sure you want to remove the root folder '{path}'? Files and folders will not be deleted from disk, and series in this root folder will not be removed from {appName}.",
"RemoveSelected": "Remove Selected", "RemoveSelected": "Remove Selected",
"RemoveSelectedBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?", "RemoveSelectedBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?",
"RemoveSelectedItem": "Remove Selected Item", "RemoveSelectedItem": "Remove Selected Item",
@@ -2140,6 +2154,9 @@
"UsenetDelayTime": "Usenet Delay: {usenetDelay}", "UsenetDelayTime": "Usenet Delay: {usenetDelay}",
"UsenetDisabled": "Usenet Disabled", "UsenetDisabled": "Usenet Disabled",
"UserInvokedSearch": "User Invoked Search", "UserInvokedSearch": "User Invoked Search",
"UserRejectedExtensions": "Additional Rejected File Extensions",
"UserRejectedExtensionsHelpText": "Comma separated list of files extensions to fail (Fail Downloads also needs to be enabled per indexer)",
"UserRejectedExtensionsTextsExamples": "Examples: '.ext, .xyz' or 'ext,xyz'",
"Username": "Username", "Username": "Username",
"UtcAirDate": "UTC Air Date", "UtcAirDate": "UTC Air Date",
"Version": "Version", "Version": "Version",
+10 -2
View File
@@ -1945,7 +1945,7 @@
"NotificationsTwitterSettingsDirectMessageHelpText": "Envía un mensaje directo en lugar de un mensaje público", "NotificationsTwitterSettingsDirectMessageHelpText": "Envía un mensaje directo en lugar de un mensaje público",
"OnApplicationUpdate": "Al actualizar la aplicación", "OnApplicationUpdate": "Al actualizar la aplicación",
"OnSeriesAdd": "Al añadir series", "OnSeriesAdd": "Al añadir series",
"OnlyForBulkSeasonReleases": "Solo para lanzamientos de temporada a granel", "OnlyForBulkSeasonReleases": "Solo para lanzamientos de temporada en bloque",
"OrganizeModalHeaderSeason": "Organizar y renombrar - {season}", "OrganizeModalHeaderSeason": "Organizar y renombrar - {season}",
"OverrideGrabNoEpisode": "Al menos un episodio debe ser seleccionado", "OverrideGrabNoEpisode": "Al menos un episodio debe ser seleccionado",
"OverrideGrabNoQuality": "La calidad debe ser seleccionada", "OverrideGrabNoQuality": "La calidad debe ser seleccionada",
@@ -2163,5 +2163,13 @@
"CloneImportList": "Clonar lista de importación", "CloneImportList": "Clonar lista de importación",
"AutoTaggingSpecificationNetwork": "Red(es)", "AutoTaggingSpecificationNetwork": "Red(es)",
"NotificationsAppriseSettingsIncludePoster": "Incluir póster", "NotificationsAppriseSettingsIncludePoster": "Incluir póster",
"NotificationsAppriseSettingsIncludePosterHelpText": "Incluir póster en el mensaje" "NotificationsAppriseSettingsIncludePosterHelpText": "Incluir póster en el mensaje",
"EpisodeMonitoring": "Monitorización de episodios",
"MonitorEpisodes": "Monitorizar episodios",
"MonitorEpisodesModalInfo": "Esta opción solo ajustará qué episodios o temporadas son monitorizados en las series. Seleccionar Ninguno dejará de monitorizar las series",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Añadir etiquetas de series",
"UserRejectedExtensions": "Extensiones adicionales de archivo rechazadas",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Añade etiquetas de series a los nuevos torrents añadidos al cliente de descarga (qBittorrent 4.1.0+)",
"UserRejectedExtensionsTextsExamples": "Ejemplos: '.ext, .xyz' o 'ext,xyz'",
"UserRejectedExtensionsHelpText": "Lista de extensiones de archivo a fallar separadas por coma (Descargas fallidas también necesita ser activado por indexador)"
} }
+18 -8
View File
@@ -816,7 +816,7 @@
"CollapseMultipleEpisodesHelpText": "Tiivistä useat samana päivänä esitettävät jaksot.", "CollapseMultipleEpisodesHelpText": "Tiivistä useat samana päivänä esitettävät jaksot.",
"CalendarLegendSeriesFinaleTooltip": "Sarjan tai kauden päätösjakso", "CalendarLegendSeriesFinaleTooltip": "Sarjan tai kauden päätösjakso",
"CalendarLegendSeriesPremiereTooltip": "Sarjan tai kauden pilottijakso", "CalendarLegendSeriesPremiereTooltip": "Sarjan tai kauden pilottijakso",
"ClickToChangeSeries": "Muuta sarjaa klikkaamalla", "ClickToChangeSeries": "Vaihda sarja klikkaamalla",
"CloneIndexer": "Monista hakupalvelu", "CloneIndexer": "Monista hakupalvelu",
"Close": "Sulje", "Close": "Sulje",
"ClearBlocklist": "Tyhjennä estolista", "ClearBlocklist": "Tyhjennä estolista",
@@ -1190,7 +1190,7 @@
"AddedDate": "Lisätty: {date}", "AddedDate": "Lisätty: {date}",
"Anime": "Anime", "Anime": "Anime",
"Any": "Mikä tahansa", "Any": "Mikä tahansa",
"ClickToChangeSeason": "Vaihda tuotantokautta painamalla tästä", "ClickToChangeSeason": "Vaihda tuotantokausi klikkaamalla",
"CountSelectedFile": "{selectedCount} tiedosto on valittu", "CountSelectedFile": "{selectedCount} tiedosto on valittu",
"SingleEpisodeInvalidFormat": "Yksittäinen jakso: virheellinen kaava", "SingleEpisodeInvalidFormat": "Yksittäinen jakso: virheellinen kaava",
"Underscore": "Alaviiva", "Underscore": "Alaviiva",
@@ -1242,7 +1242,7 @@
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Vahvista uusi salasana", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Vahvista uusi salasana",
"Category": "Kategoria", "Category": "Kategoria",
"ChownGroup": "chown-ryhmä", "ChownGroup": "chown-ryhmä",
"ClickToChangeEpisode": "Vaihda jaksoa painamalla tästä", "ClickToChangeEpisode": "Vaihda jakso klikkaamalla",
"CompletedDownloadHandling": "Valmistuneiden latausten käsittely", "CompletedDownloadHandling": "Valmistuneiden latausten käsittely",
"Condition": "Ehto", "Condition": "Ehto",
"Continuing": "Jatkuu", "Continuing": "Jatkuu",
@@ -1834,13 +1834,13 @@
"ImportListsMyAnimeListSettingsListStatus": "Listan tila", "ImportListsMyAnimeListSettingsListStatus": "Listan tila",
"ImportListStatusAllUnavailableHealthCheckMessage": "Mitkään listat eivät ole virheiden vuoksi käytettävissä", "ImportListStatusAllUnavailableHealthCheckMessage": "Mitkään listat eivät ole virheiden vuoksi käytettävissä",
"MetadataKometaDeprecatedSetting": "Poistunut", "MetadataKometaDeprecatedSetting": "Poistunut",
"NotificationsTelegramSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.", "NotificationsTelegramSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit median metatietoihin.",
"OnFileImport": "Kun tiedosto tuodaan", "OnFileImport": "Kun tiedosto tuodaan",
"OnFileUpgrade": "Kun tiedosto päivitetään", "OnFileUpgrade": "Kun tiedosto päivitetään",
"ReleaseProfile": "Julkaisuprofiili", "ReleaseProfile": "Julkaisuprofiili",
"ShowTags": "Näytä tunnisteet", "ShowTags": "Näytä tunnisteet",
"TodayAt": "Tänään klo {time}", "TodayAt": "Tänään klo {time}",
"ClickToChangeReleaseType": "Vaihda julkaisun tyyppiä painamalla tästä", "ClickToChangeReleaseType": "Vaihda julkaisun tyyppi klikkaamalla",
"CustomFormatsSpecificationSource": "Lähde", "CustomFormatsSpecificationSource": "Lähde",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent ilmoittaa puuttuvista tiedostoista", "DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent ilmoittaa puuttuvista tiedostoista",
"DownloadClientSabnzbdValidationCheckBeforeDownload": "Poista SABnbzd:n \"Tarkista ennen lataamista\" -asetus käytöstä", "DownloadClientSabnzbdValidationCheckBeforeDownload": "Poista SABnbzd:n \"Tarkista ennen lataamista\" -asetus käytöstä",
@@ -1862,7 +1862,7 @@
"ReleaseGroupFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{Julkaisuryhmä:30}\"), että alusta (esim. \"{Julkaisuryhmä:-30}\") tuetaan.", "ReleaseGroupFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{Julkaisuryhmä:30}\"), että alusta (esim. \"{Julkaisuryhmä:-30}\") tuetaan.",
"InstallMajorVersionUpdateMessage": "Tämä päivitys asentaa uuden pääversion, joka ei välttämättä ole yhteensopiva laitteistosi kanssa. Haluatko varmasti asentaa päivityksen?", "InstallMajorVersionUpdateMessage": "Tämä päivitys asentaa uuden pääversion, joka ei välttämättä ole yhteensopiva laitteistosi kanssa. Haluatko varmasti asentaa päivityksen?",
"MinimumCustomFormatScoreIncrementHelpText": "Pienin vaadittu olemassa olevien ja uusien julkaisujen välinen mukautetun muodon pisteytyksen korotus ennen kuin {appName} tulkitsee julkaisun päivitykseksi.", "MinimumCustomFormatScoreIncrementHelpText": "Pienin vaadittu olemassa olevien ja uusien julkaisujen välinen mukautetun muodon pisteytyksen korotus ennen kuin {appName} tulkitsee julkaisun päivitykseksi.",
"NotificationsGotifySettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.", "NotificationsGotifySettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit median metatietoihin.",
"NotificationsPlexSettingsServerHelpText": "Valitse tunnistautumisen jälkeen palvelin Plex.tv-tililtä.", "NotificationsPlexSettingsServerHelpText": "Valitse tunnistautumisen jälkeen palvelin Plex.tv-tililtä.",
"EpisodeTitleFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{jakson nimi:30}\"), että alusta (esim. \"{jakson nimi:-30}\") tuetaan. Tarvittaessa jaksojen nimet lyhennetään automaattisesti järjestelmän rajoitukseen.", "EpisodeTitleFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{jakson nimi:30}\"), että alusta (esim. \"{jakson nimi:-30}\") tuetaan. Tarvittaessa jaksojen nimet lyhennetään automaattisesti järjestelmän rajoitukseen.",
"SeriesFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{Sarjan nimi:30}\"), että alusta (esim. \"{Sarjan nimi:-30}\") tuetaan.", "SeriesFootNote": "Vaihtoehtoisesti voit hallita lyhennystä tavujen enimmäismäärän perusteella, ellipsi (...) mukaan lukien. Sekä lyhennystä lopusta (esim. \"{Sarjan nimi:30}\"), että alusta (esim. \"{Sarjan nimi:-30}\") tuetaan.",
@@ -2156,10 +2156,20 @@
"NotificationsPushcutSettingsIncludePoster": "Sisällytä juliste", "NotificationsPushcutSettingsIncludePoster": "Sisällytä juliste",
"NotificationsPushcutSettingsIncludePosterHelpText": "Näytä juliste ilmoituksessa.", "NotificationsPushcutSettingsIncludePosterHelpText": "Näytä juliste ilmoituksessa.",
"NotificationsPushcutSettingsMetadataLinks": "Metatietolinkit", "NotificationsPushcutSettingsMetadataLinks": "Metatietolinkit",
"NotificationsPushcutSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.", "NotificationsPushcutSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit median metatietoihin.",
"AutoTaggingSpecificationNetwork": "Verkot", "AutoTaggingSpecificationNetwork": "Verkot",
"DownloadClientItemErrorMessage": "{clientName} ilmoittaa virheestä: {message}", "DownloadClientItemErrorMessage": "{clientName} ilmoittaa virheestä: {message}",
"EpisodesInSeason": "Tuotantokaudessa on {episodeCount} jaksoa", "EpisodesInSeason": "Tuotantokaudessa on {episodeCount} jaksoa",
"CloneImportList": "Monista tuontilista", "CloneImportList": "Monista tuontilista",
"DefaultNameCopiedImportList": "{name} (kopio)" "DefaultNameCopiedImportList": "{name} (kopio)",
"EpisodeMonitoring": "Jaksojen valvonta",
"MonitorEpisodes": "Valvo jaksoja",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Merkitse uudet latauspalveluun lisätyt torrentit sarjatunnisteilla (aBittorrent 4.1.0+).",
"MonitorEpisodesModalInfo": "Tämä määrittää vain mitä jaksoja tai kausia sarjasta valvotaan. Valinta \"Ei mitään\" lopettaa sarjan valvonnan.",
"NotificationsAppriseSettingsIncludePoster": "Sisällytä juliste",
"NotificationsAppriseSettingsIncludePosterHelpText": "Sisällytä julisteet viesteihin.",
"UserRejectedExtensions": "Lisää estettyjä tiedostopäätteitä",
"UserRejectedExtensionsHelpText": "Pilkuin eroteltu listaus hylättävistä tiedostopäätteistä. Lisäksi \"Hylättävät lautaukset\"-asetuksen tulee olla käytössä hakupalvelukohtaisesti.",
"UserRejectedExtensionsTextsExamples": "Esimerkiksi: \".ext, .xyz\" tai \"ext,xyz\".",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Lisää sarjan tunnisteet"
} }
+44 -4
View File
@@ -444,7 +444,7 @@
"NoSeriesFoundImportOrAdd": "Aucune série trouvée. Pour commencer, vous souhaiterez importer votre série existante ou ajouter une nouvelle série.", "NoSeriesFoundImportOrAdd": "Aucune série trouvée. Pour commencer, vous souhaiterez importer votre série existante ou ajouter une nouvelle série.",
"ICalFeedHelpText": "Copiez cette URL dans votre/vos client(s) ou cliquez pour abonner si votre navigateur est compatible avec webcal", "ICalFeedHelpText": "Copiez cette URL dans votre/vos client(s) ou cliquez pour abonner si votre navigateur est compatible avec webcal",
"SeasonFolderFormat": "Format du dossier de saison", "SeasonFolderFormat": "Format du dossier de saison",
"QualitiesHelpText": "Les qualités plus élevées dans la liste sont plus préférées. Les qualités au sein dun même groupe sont égales. Seules les qualités vérifiées sont recherchées", "QualitiesHelpText": "Les qualités placées en haut de la liste sont privilégiées même si elles ne sont pas cochées. Les qualités d'un même groupe sont égales. Seules les qualités cochées sont recherchées",
"PrioritySettings": "Priorité : {priority}", "PrioritySettings": "Priorité : {priority}",
"ImportExistingSeries": "Importer une série existante", "ImportExistingSeries": "Importer une série existante",
"RootFolderSelectFreeSpace": "{freeSpace} Libre", "RootFolderSelectFreeSpace": "{freeSpace} Libre",
@@ -677,7 +677,7 @@
"RootFolderMultipleMissingHealthCheckMessage": "Plusieurs dossiers racine sont manquants : {rootFolderPaths}", "RootFolderMultipleMissingHealthCheckMessage": "Plusieurs dossiers racine sont manquants : {rootFolderPaths}",
"RssIsNotSupportedWithThisIndexer": "RSS n'est pas pris en charge avec cet indexeur", "RssIsNotSupportedWithThisIndexer": "RSS n'est pas pris en charge avec cet indexeur",
"RssSyncInterval": "Intervalle de synchronisation RSS", "RssSyncInterval": "Intervalle de synchronisation RSS",
"RssSyncIntervalHelpText": "Intervalle en minutes. Réglez sur zéro pour désactiver (cela arrêtera toute capture de libération automatique)", "RssSyncIntervalHelpText": "Intervalle en minutes. Réglez sur zéro pour désactiver (cela arrêtera toute capture de release automatique)",
"RssSyncIntervalHelpTextWarning": "Cela s'appliquera à tous les indexeurs, veuillez suivre les règles énoncées par eux", "RssSyncIntervalHelpTextWarning": "Cela s'appliquera à tous les indexeurs, veuillez suivre les règles énoncées par eux",
"SaveChanges": "Sauvegarder les modifications", "SaveChanges": "Sauvegarder les modifications",
"SceneNumbering": "Numérotation des scènes", "SceneNumbering": "Numérotation des scènes",
@@ -2017,7 +2017,7 @@
"ImportListsTraktSettingsPopularListTypeTrendingShows": "Spectacles en vogue", "ImportListsTraktSettingsPopularListTypeTrendingShows": "Spectacles en vogue",
"ImportListsTraktSettingsPopularName": "Liste populaire de Trakt", "ImportListsTraktSettingsPopularName": "Liste populaire de Trakt",
"ImportListsTraktSettingsRating": "Evaluation", "ImportListsTraktSettingsRating": "Evaluation",
"ImportListsTraktSettingsRatingSeriesHelpText": "Série de filtres par plage de valeurs nominales (0-100)", "ImportListsTraktSettingsRatingSeriesHelpText": "Filtrer les séries par plage de classement (0-100)",
"ImportListsTraktSettingsWatchedListFilterSeriesHelpText": "Si le type de liste est surveillé, sélectionnez le type de série que vous souhaitez importer", "ImportListsTraktSettingsWatchedListFilterSeriesHelpText": "Si le type de liste est surveillé, sélectionnez le type de série que vous souhaitez importer",
"ImportListsTraktSettingsWatchListSorting": "Tri de la liste de surveillance", "ImportListsTraktSettingsWatchListSorting": "Tri de la liste de surveillance",
"ImportListsTraktSettingsWatchListSortingHelpText": "Si le type de liste est surveillé, sélectionnez l'ordre de tri de la liste", "ImportListsTraktSettingsWatchListSortingHelpText": "Si le type de liste est surveillé, sélectionnez l'ordre de tri de la liste",
@@ -2123,5 +2123,45 @@
"LastSearched": "Dernière recherche", "LastSearched": "Dernière recherche",
"FolderNameTokens": "Jetons de nom de dossier", "FolderNameTokens": "Jetons de nom de dossier",
"ManageCustomFormats": "Gérer les formats personnalisés", "ManageCustomFormats": "Gérer les formats personnalisés",
"Menu": "Menu" "Menu": "Menu",
"Fallback": "Alternative",
"MetadataKometaDeprecatedSetting": "Obsolète",
"AutoTaggingSpecificationNetwork": "Réseau(x)",
"DefaultNameCopiedImportList": "{name} - Copie",
"DownloadClientItemErrorMessage": "{clientName} a rapporté une erreur : {message}",
"EditSizes": "Modifier les dimensions",
"NotificationsGotifySettingsPreferredMetadataLink": "Lien de métadonnées préféré",
"NotificationsPushcutSettingsMetadataLinksHelpText": "Ajouter un lien vers les métadonnées de la série lors de l'envoie d'une notification",
"NotificationsTelegramSettingsLinkPreviewHelpText": "Détermine quel lien sera aperçu dans la notification Telegram. Choisir 'Aucun' pour désactiver",
"DoneEditingSizes": "Terminer la modification des dimensions",
"EpisodeMonitoring": "Suivi des épisodes",
"ManageFormats": "Gérer les formats",
"MinuteShorthand": "m",
"MonitorEpisodes": "Surveiller les épisodes",
"NotificationsGotifySettingsPreferredMetadataLinkHelpText": "Lien de métadonnées pour les clients qui ne peuvent avoir qu'un seul lien",
"NotificationsSettingsWebhookHeaders": "En-têtes",
"NotificationsTelegramSettingsIncludeInstanceNameHelpText": "Inclure le nom de l'instance dans la notification de façon facultative",
"EpisodesInSeason": "{episodeCount} épisodes dans la saison",
"FileSize": "Taille de fichier",
"Maximum": "Maximum",
"Minimum": "Minimum",
"MinimumCustomFormatScoreIncrement": "Incrément minimal du score du format personnalisé",
"Minute": "minute",
"NotificationsPushcutSettingsIncludePoster": "Inclure l'affiche",
"NotificationsPushcutSettingsIncludePosterHelpText": "Inclure l'affiche avec les notifications",
"NotificationsTelegramSettingsLinkPreview": "Aperçu du lien",
"FavoriteFolderAdd": "Ajouter un dossier favori",
"FavoriteFolderRemove": "Supprimer le dossier favori",
"DownloadClientUTorrentProviderMessage": "uTorrent a l'habitude d'inclure des cryptomineurs, des logiciels malveillants et des publicités, nous vous encourageons fortement à choisir un client différent.",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Ajouter des tags de séries",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Ajouter des tags de séries aux nouveaux torrents ajoutés au client de téléchargement (qBittorrent 4.1.0+)",
"FavoriteFolders": "Dossier favori",
"MinimumCustomFormatScoreIncrementHelpText": "Amélioration minimale requise du score de format personnalisé entre les versions existantes et nouvelles avant que {appName} ne le considère comme une mise à niveau",
"MonitorEpisodesModalInfo": "Ce paramètre n'ajustera que les épisodes ou saisons qui seront surveillés dans une série. Sélectionner Aucun retirera la surveillance de la série",
"NotificationsTelegramSettingsIncludeInstanceName": "Inclure le nom de l'instance dans le titre",
"NotificationsPushcutSettingsMetadataLinks": "Lien de métadonnées",
"UserRejectedExtensions": "Extensions de fichiers rejetées supplémentaires",
"UserRejectedExtensionsHelpText": "Liste séparée par des virgules des extensions de fichiers à échouer (“Échouer les téléchargements” doit également être activé dans lindexeur)",
"UserRejectedExtensionsTextsExamples": "Examples : '.ext, .xyz' or 'ext,xyz'",
"Warning": "Avertissement"
} }
+6 -1
View File
@@ -58,5 +58,10 @@
"AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime", "AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime",
"AddConditionError": "Neuspješno dodavanje novog uvjeta, molimo pokušaj ponovno.", "AddConditionError": "Neuspješno dodavanje novog uvjeta, molimo pokušaj ponovno.",
"AddIndexerImplementation": "Dodaj Indexer - {implementationName}", "AddIndexerImplementation": "Dodaj Indexer - {implementationName}",
"AuthenticationRequiredWarning": "Kako bi se spriječio udaljeni pristup bez autentikacije, {appName} sad zahtjeva da autentikacija bude omogućena. Izborno se može onemogućiti autentikacija s lokalnih adresa." "AuthenticationRequiredWarning": "Kako bi se spriječio udaljeni pristup bez autentikacije, {appName} sad zahtjeva da autentikacija bude omogućena. Izborno se može onemogućiti autentikacija s lokalnih adresa.",
"AddANewPath": "Dodaj novu putanju",
"AddCustomFilter": "Dodaj proizvoljan filter",
"AddCustomFormat": "Dodaj proizvoljan format",
"Add": "Dodaj",
"Activity": "Aktivnost"
} }
+19 -11
View File
@@ -326,7 +326,7 @@
"Grabbed": "Obtido", "Grabbed": "Obtido",
"Ignored": "Ignorado", "Ignored": "Ignorado",
"Imported": "Importado", "Imported": "Importado",
"IncludeUnmonitored": "Incluir não monitorados", "IncludeUnmonitored": "Incluir Não Monitorados",
"Indexer": "Indexador", "Indexer": "Indexador",
"LatestSeason": "Temporada mais recente", "LatestSeason": "Temporada mais recente",
"MissingEpisodes": "Episódios ausentes", "MissingEpisodes": "Episódios ausentes",
@@ -383,7 +383,7 @@
"LastUsed": "Usado por último", "LastUsed": "Usado por último",
"MoveAutomatically": "Mover automaticamente", "MoveAutomatically": "Mover automaticamente",
"NoResultsFound": "Nenhum resultado encontrado", "NoResultsFound": "Nenhum resultado encontrado",
"RemoveRootFolder": "Remover pasta raiz", "RemoveRootFolder": "Remover Pasta Raiz",
"RemoveTagsAutomatically": "Remover Tags Automaticamente", "RemoveTagsAutomatically": "Remover Tags Automaticamente",
"Season": "Temporada", "Season": "Temporada",
"SelectFolder": "Selecionar Pasta", "SelectFolder": "Selecionar Pasta",
@@ -919,7 +919,7 @@
"VisitTheWikiForMoreDetails": "Visite o wiki para mais detalhes: ", "VisitTheWikiForMoreDetails": "Visite o wiki para mais detalhes: ",
"WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)", "WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)",
"UnmonitorSpecialEpisodes": "Não Monitorar Especiais", "UnmonitorSpecialEpisodes": "Não Monitorar Especiais",
"MonitorAllEpisodes": "Todos os episódios", "MonitorAllEpisodes": "Todos os Episódios",
"AddNewSeries": "Adicionar nova série", "AddNewSeries": "Adicionar nova série",
"AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que deseja acrescentar.", "AddNewSeriesHelpText": "É fácil adicionar uma nova série, basta começar a digitar o nome da série que deseja acrescentar.",
"AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos limites não foram atendidos", "AddNewSeriesSearchForCutoffUnmetEpisodes": "Iniciar a pesquisa por episódios cujos limites não foram atendidos",
@@ -942,7 +942,7 @@
"LibraryImportTipsQualityInEpisodeFilename": "Certifique-se de que seus arquivos incluam a qualidade nos nomes de arquivo. Por exemplo: \"episódio.s02e15.bluray.mkv\"", "LibraryImportTipsQualityInEpisodeFilename": "Certifique-se de que seus arquivos incluam a qualidade nos nomes de arquivo. Por exemplo: \"episódio.s02e15.bluray.mkv\"",
"Monitor": "Monitorar", "Monitor": "Monitorar",
"MonitorAllEpisodesDescription": "Monitorar todos os episódios, exceto os especiais", "MonitorAllEpisodesDescription": "Monitorar todos os episódios, exceto os especiais",
"MonitorExistingEpisodes": "Episódios existentes", "MonitorExistingEpisodes": "Episódios Existentes",
"MonitorFirstSeason": "Primeira temporada", "MonitorFirstSeason": "Primeira temporada",
"MonitorFirstSeasonDescription": "Monitorar todos os episódios da primeira temporada. As demais temporadas serão ignoradas", "MonitorFirstSeasonDescription": "Monitorar todos os episódios da primeira temporada. As demais temporadas serão ignoradas",
"MonitorFutureEpisodes": "Futuros episódios", "MonitorFutureEpisodes": "Futuros episódios",
@@ -1510,7 +1510,7 @@
"DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as etiquetas iniciais. Isso evita conflitos com downloads não relacionados.", "DownloadClientFloodSettingsTagsHelpText": "Etiquetas iniciais de um download. Para ser reconhecido, um download deve ter todas as etiquetas iniciais. Isso evita conflitos com downloads não relacionados.",
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.", "DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.",
"DownloadClientFloodSettingsPostImportTagsHelpText": "Acrescenta etiquetas após a importação de um download.", "DownloadClientFloodSettingsPostImportTagsHelpText": "Acrescenta etiquetas após a importação de um download.",
"BlackholeWatchFolder": "Pasta de monitoramento", "BlackholeWatchFolder": "Pasta de Monitoramento",
"BlackholeWatchFolderHelpText": "Pasta da qual o {appName} deve importar os downloads concluídos", "BlackholeWatchFolderHelpText": "Pasta da qual o {appName} deve importar os downloads concluídos",
"Category": "Categoria", "Category": "Categoria",
"Directory": "Diretório", "Directory": "Diretório",
@@ -1675,7 +1675,7 @@
"PasswordConfirmation": "Confirmação Da Senha", "PasswordConfirmation": "Confirmação Da Senha",
"MonitorPilotEpisodeDescription": "Monitorar apenas o primeiro episódio da primeira temporada", "MonitorPilotEpisodeDescription": "Monitorar apenas o primeiro episódio da primeira temporada",
"MonitorNoNewSeasonsDescription": "Não monitorar nenhuma nova temporada automaticamente", "MonitorNoNewSeasonsDescription": "Não monitorar nenhuma nova temporada automaticamente",
"MonitorAllSeasons": "Todas as temporadas", "MonitorAllSeasons": "Todas as Temporadas",
"MonitorAllSeasonsDescription": "Monitorar todas as novas temporadas automaticamente", "MonitorAllSeasonsDescription": "Monitorar todas as novas temporadas automaticamente",
"MonitorLastSeason": "Última temporada", "MonitorLastSeason": "Última temporada",
"MonitorLastSeasonDescription": "Monitorar todos os episódios da última temporada", "MonitorLastSeasonDescription": "Monitorar todos os episódios da última temporada",
@@ -2019,7 +2019,7 @@
"IgnoreDownload": "Ignorar download", "IgnoreDownload": "Ignorar download",
"ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais", "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais",
"KeepAndTagSeries": "Manter e etiquetar séries", "KeepAndTagSeries": "Manter e etiquetar séries",
"KeepAndUnmonitorSeries": "Manter e desmonitorar séries", "KeepAndUnmonitorSeries": "Manter e Não Monitorar Séries",
"ListSyncLevelHelpText": "As séries na biblioteca serão tratadas com base na sua seleção se saírem de sua(s) lista(s) ou não aparecerem nela(s)", "ListSyncLevelHelpText": "As séries na biblioteca serão tratadas com base na sua seleção se saírem de sua(s) lista(s) ou não aparecerem nela(s)",
"ListSyncTag": "Etiqueta de sincronização de lista", "ListSyncTag": "Etiqueta de sincronização de lista",
"ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série sair ou não estiver mais na(s) sua(s) lista(s)", "ListSyncTagHelpText": "Esta etiqueta será adicionada quando uma série sair ou não estiver mais na(s) sua(s) lista(s)",
@@ -2032,8 +2032,8 @@
"CustomFormatsSpecificationFlag": "Sinalizador", "CustomFormatsSpecificationFlag": "Sinalizador",
"IndexerFlags": "Sinalizadores do indexador", "IndexerFlags": "Sinalizadores do indexador",
"SetIndexerFlags": "Definir Sinalizadores de Indexador", "SetIndexerFlags": "Definir Sinalizadores de Indexador",
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar monitoramento da temporada", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar Monitoramento da Temporada",
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronizar o monitoramento da temporada da instância do {appName}. Se ativada, \"Monitorar\" será ignorado", "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronizar o monitoramento da temporada da instância do {appName}. Se ativada, 'Monitorar' será ignorado",
"CustomFilter": "Filtro personalizado", "CustomFilter": "Filtro personalizado",
"Filters": "Filtros", "Filters": "Filtros",
"Label": "Rótulo", "Label": "Rótulo",
@@ -2065,7 +2065,7 @@
"DayOfWeekAt": "{day} às {time}", "DayOfWeekAt": "{day} às {time}",
"TodayAt": "Hoje às {time}", "TodayAt": "Hoje às {time}",
"TomorrowAt": "Amanhã às {time}", "TomorrowAt": "Amanhã às {time}",
"HasUnmonitoredSeason": "Há temporadas não monitoradas", "HasUnmonitoredSeason": "Há Temporadas Não Monitoradas",
"YesterdayAt": "Ontem às {time}", "YesterdayAt": "Ontem às {time}",
"UnableToImportAutomatically": "Não foi possível importar automaticamente", "UnableToImportAutomatically": "Não foi possível importar automaticamente",
"CustomColonReplacement": "Personalizar substituto do dois-pontos", "CustomColonReplacement": "Personalizar substituto do dois-pontos",
@@ -2163,5 +2163,13 @@
"EpisodesInSeason": "{episodeCount} episódios na temporada", "EpisodesInSeason": "{episodeCount} episódios na temporada",
"AutoTaggingSpecificationNetwork": "Rede(s)", "AutoTaggingSpecificationNetwork": "Rede(s)",
"NotificationsAppriseSettingsIncludePoster": "Incluir Pôster", "NotificationsAppriseSettingsIncludePoster": "Incluir Pôster",
"NotificationsAppriseSettingsIncludePosterHelpText": "Incluir pôster na mensagem" "NotificationsAppriseSettingsIncludePosterHelpText": "Incluir pôster na mensagem",
"EpisodeMonitoring": "Monitoramento do Episódio",
"MonitorEpisodes": "Monitorar Episódios",
"MonitorEpisodesModalInfo": "Esta configuração ajustará apenas quais episódios ou temporadas serão monitorados dentro de uma série. Selecionar Nenhum desativará o monitoramento da série",
"UserRejectedExtensions": "Extensões de Arquivos Rejeitadas Adicionais",
"UserRejectedExtensionsHelpText": "Lista separada por vírgulas de extensões de arquivos para falhar (Falha em downloads também precisa ser habilitado por indexador)",
"UserRejectedExtensionsTextsExamples": "Exemplos: '.ext, .xyz' or 'ext,xyz'",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Adicionar etiquetas das séries a novos torrents adicionados ao cliente de download (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Adicionar Etiquetas das Séries"
} }
+15 -3
View File
@@ -25,7 +25,7 @@
"Close": "Închide", "Close": "Închide",
"Delete": "Șterge", "Delete": "Șterge",
"Added": "Adăugat", "Added": "Adăugat",
"CountSeasons": "{count} sezoane", "CountSeasons": "{count} Sezoane",
"DownloadClientStatusSingleClientHealthCheckMessage": "Clienții de descărcare indisponibili datorită erorilor: {downloadClientNames}", "DownloadClientStatusSingleClientHealthCheckMessage": "Clienții de descărcare indisponibili datorită erorilor: {downloadClientNames}",
"EnableAutomaticSearch": "Activați căutarea automată", "EnableAutomaticSearch": "Activați căutarea automată",
"EnableInteractiveSearch": "Activați căutarea interactivă", "EnableInteractiveSearch": "Activați căutarea interactivă",
@@ -172,7 +172,7 @@
"HistoryLoadError": "Istoricul nu poate fi încărcat", "HistoryLoadError": "Istoricul nu poate fi încărcat",
"DefaultNameCopiedSpecification": "{name} - Copie", "DefaultNameCopiedSpecification": "{name} - Copie",
"DefaultNameCopiedProfile": "{name} - Copie", "DefaultNameCopiedProfile": "{name} - Copie",
"DeletedReasonManual": "Fișierul a fost șters prin interfața de utilizare", "DeletedReasonManual": "Fișierul a fost șters folosind {appName}, fie manual, fie de către un alt instrument prin API",
"NoHistoryFound": "Nu s-a găsit istoric", "NoHistoryFound": "Nu s-a găsit istoric",
"Or": "sau", "Or": "sau",
"PendingDownloadClientUnavailable": "În așteptare - Clientul de descărcare nu este disponibil", "PendingDownloadClientUnavailable": "În așteptare - Clientul de descărcare nu este disponibil",
@@ -212,5 +212,17 @@
"Clone": "Clonează", "Clone": "Clonează",
"DownloadClientSettingsOlderPriority": "Prioritate mai vechi", "DownloadClientSettingsOlderPriority": "Prioritate mai vechi",
"DownloadClientSettingsRecentPriority": "Prioritate recente", "DownloadClientSettingsRecentPriority": "Prioritate recente",
"Absolute": "Absolut" "Absolute": "Absolut",
"Pending": "În așteptare",
"YesterdayAt": "Ieri la {time}",
"YesCancel": "Da, Anulează",
"CalendarOptions": "Setări Calendar",
"Yesterday": "Ieri",
"Any": "Oricare",
"AddCondition": "Adaugă Condiție",
"AddImportList": "Adaugă Lista de Import",
"Week": "Săptămână",
"WhatsNew": "Ce-i nou?",
"Warning": "Avertisment",
"AddAutoTag": "Adaugă Etichetă Automată"
} }
+14 -5
View File
@@ -269,7 +269,7 @@
"AnimeEpisodeFormat": "Формат аниме-эпизода", "AnimeEpisodeFormat": "Формат аниме-эпизода",
"AuthBasic": "Базовый (Всплывающее окно браузера)", "AuthBasic": "Базовый (Всплывающее окно браузера)",
"AuthForm": "Формы (Страница авторизации)", "AuthForm": "Формы (Страница авторизации)",
"Authentication": "Авторизация", "Authentication": "Аутентификация",
"AuthenticationRequired": "Требуется авторизация", "AuthenticationRequired": "Требуется авторизация",
"BackupIntervalHelpText": "Периодичность автоматического резервного копирования", "BackupIntervalHelpText": "Периодичность автоматического резервного копирования",
"BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены", "BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены",
@@ -1500,7 +1500,7 @@
"RejectionCount": "Количество отказов", "RejectionCount": "Количество отказов",
"Release": "Релиз", "Release": "Релиз",
"ReleaseGroup": "Релиз группа", "ReleaseGroup": "Релиз группа",
"ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).`).", "ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).",
"ReleaseProfileIndexerHelpText": "Укажите, к какому индексатору применяется профиль", "ReleaseProfileIndexerHelpText": "Укажите, к какому индексатору применяется профиль",
"ReleaseProfileIndexerHelpTextWarning": "Установка определенного индексатора в профиле релиза приведет к тому, что этот профиль будет применяться только к релизам из этого индексатора.", "ReleaseProfileIndexerHelpTextWarning": "Установка определенного индексатора в профиле релиза приведет к тому, что этот профиль будет применяться только к релизам из этого индексатора.",
"ReleaseProfiles": "Профили релизов", "ReleaseProfiles": "Профили релизов",
@@ -1696,7 +1696,7 @@
"MetadataSettings": "Настройки метаданных", "MetadataSettings": "Настройки метаданных",
"NotificationsAppriseSettingsNotificationType": "Тип информирования об уведомлении", "NotificationsAppriseSettingsNotificationType": "Тип информирования об уведомлении",
"NotificationsEmailSettingsFromAddress": "С адреса", "NotificationsEmailSettingsFromAddress": "С адреса",
"NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование.", "NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование",
"NotificationsCustomScriptSettingsProviderMessage": "При тестировании будет выполняться сценарий с типом события, установленным на {eventTypeTest}. Убедитесь, что ваш сценарий обрабатывает это правильно", "NotificationsCustomScriptSettingsProviderMessage": "При тестировании будет выполняться сценарий с типом события, установленным на {eventTypeTest}. Убедитесь, что ваш сценарий обрабатывает это правильно",
"NotificationsJoinSettingsApiKeyHelpText": "Ключ API из настроек вашей учетной записи присоединения (нажмите кнопку «Присоединиться к API»).", "NotificationsJoinSettingsApiKeyHelpText": "Ключ API из настроек вашей учетной записи присоединения (нажмите кнопку «Присоединиться к API»).",
"NotificationsGotifySettingsServerHelpText": "URL-адрес сервера Gotify, включая http(s):// и порт, если необходимо", "NotificationsGotifySettingsServerHelpText": "URL-адрес сервера Gotify, включая http(s):// и порт, если необходимо",
@@ -2016,7 +2016,7 @@
"Search": "Поиск", "Search": "Поиск",
"RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит интерфейс пользователя во время процесса восстановления.", "RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит интерфейс пользователя во время процесса восстановления.",
"HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, перейдя по ссылке wiki (значок книги) в конце строки или проверить [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.", "HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, перейдя по ссылке wiki (значок книги) в конце строки или проверить [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.",
"MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github.", "MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github",
"Space": "Пробел", "Space": "Пробел",
"SslCertPasswordHelpText": "Пароль для файла pfx", "SslCertPasswordHelpText": "Пароль для файла pfx",
"SpecialEpisode": "Спец. эпизод", "SpecialEpisode": "Спец. эпизод",
@@ -2162,5 +2162,14 @@
"DownloadClientItemErrorMessage": "{clientName} сообщает об ошибке: {message}", "DownloadClientItemErrorMessage": "{clientName} сообщает об ошибке: {message}",
"AutoTaggingSpecificationNetwork": "Сеть(и)", "AutoTaggingSpecificationNetwork": "Сеть(и)",
"NotificationsAppriseSettingsIncludePoster": "Добавить постер", "NotificationsAppriseSettingsIncludePoster": "Добавить постер",
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение" "NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение",
"EpisodeMonitoring": "Отслеживание эпизода",
"MonitorEpisodes": "Отслеживать эпизоды",
"MonitorEpisodesModalInfo": "Эта настройка влияет только на отслеживание эпизодов или сезонов внутри сериала. Выбор ничего приведёт к остановке отслеживания сериала",
"ImportListsSimklSettingsUserListTypeHold": "Оставить",
"UserRejectedExtensions": "Дополнительные запрещенные расширения файлов",
"UserRejectedExtensionsTextsExamples": "Примеры: '.ext, .xyz' или 'ext,xyz'",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Добавлять теги сериалов",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Добавлять теги сериалов к новым торрентам, добавляемым в загрузчик (qBittorrent 4.1.0+)",
"UserRejectedExtensionsHelpText": "Список запрещенных расширений файлов, разделенных запятой (так же нужно включить настройку Считать загрузки неуспешными в настройках индексаторов)"
} }
+2 -1
View File
@@ -43,5 +43,6 @@
"DownloadStationStatusExtracting": "Packar upp: {progress}%", "DownloadStationStatusExtracting": "Packar upp: {progress}%",
"Duplicate": "Dubblett", "Duplicate": "Dubblett",
"Yesterday": "Igår", "Yesterday": "Igår",
"EditCustomFormat": "Redigera anpassat format" "EditCustomFormat": "Redigera anpassat format",
"AbsoluteEpisodeNumber": "Fullständigt Avsnitt Nummer"
} }
+11 -3
View File
@@ -1424,7 +1424,7 @@
"Reload": "Tekrar yükle", "Reload": "Tekrar yükle",
"RemoveFailedDownloadsHelpText": "Başarısız indirmeleri indirme istemcisi geçmişinden kaldırın", "RemoveFailedDownloadsHelpText": "Başarısız indirmeleri indirme istemcisi geçmişinden kaldırın",
"RemoveFromBlocklist": "Kara listeden kaldır", "RemoveFromBlocklist": "Kara listeden kaldır",
"RemoveRootFolder": "Kök klasörü kaldır", "RemoveRootFolder": "Kök Klasörü Kaldır",
"RemoveSelected": "Seçilenleri Kaldır", "RemoveSelected": "Seçilenleri Kaldır",
"RemovingTag": "Etiket kaldırılıyor", "RemovingTag": "Etiket kaldırılıyor",
"RenameFiles": "Yeniden Adlandır", "RenameFiles": "Yeniden Adlandır",
@@ -1791,7 +1791,7 @@
"IndexerValidationJackettAllNotSupportedHelpText": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen indeksleyicileri tek tek ekleyin", "IndexerValidationJackettAllNotSupportedHelpText": "Jackett'in tüm uç noktaları desteklenmiyor, lütfen indeksleyicileri tek tek ekleyin",
"IndexerValidationNoRssFeedQueryAvailable": "RSS besleme sorgusu mevcut değil. Bu, indeksleyici veya indeksleyici kategori ayarlarınızdan kaynaklı bir sorun olabilir.", "IndexerValidationNoRssFeedQueryAvailable": "RSS besleme sorgusu mevcut değil. Bu, indeksleyici veya indeksleyici kategori ayarlarınızdan kaynaklı bir sorun olabilir.",
"IndexerValidationUnableToConnectResolutionFailure": "İndeksleyiciye bağlanılamıyor bağlantı hatası. İndeksleyicinin sunucusuna ve DNS'ine olan bağlantınızı kontrol edin. {exceptionMessage}.", "IndexerValidationUnableToConnectResolutionFailure": "İndeksleyiciye bağlanılamıyor bağlantı hatası. İndeksleyicinin sunucusuna ve DNS'ine olan bağlantınızı kontrol edin. {exceptionMessage}.",
"IndexerSettingsFailDownloads": "Başarısız İndirmeler", "IndexerSettingsFailDownloads": "İndirmeleri Başarısız Say",
"IndexerSettingsFailDownloadsHelpText": "Tamamlanan indirmeler işlenirken {appName} bu seçili dosya türlerini başarısız indirmeler olarak değerlendirecektir.", "IndexerSettingsFailDownloadsHelpText": "Tamamlanan indirmeler işlenirken {appName} bu seçili dosya türlerini başarısız indirmeler olarak değerlendirecektir.",
"IndexerSettingsMinimumSeeders": "Minimum Seeder", "IndexerSettingsMinimumSeeders": "Minimum Seeder",
"IndexerSettingsRssUrl": "RSS URL", "IndexerSettingsRssUrl": "RSS URL",
@@ -2163,5 +2163,13 @@
"EpisodesInSeason": "Sezondaki {episodeCount} bölüm", "EpisodesInSeason": "Sezondaki {episodeCount} bölüm",
"AutoTaggingSpecificationNetwork": "Ağ(lar)", "AutoTaggingSpecificationNetwork": "Ağ(lar)",
"NotificationsAppriseSettingsIncludePoster": "Poster'i ekle", "NotificationsAppriseSettingsIncludePoster": "Poster'i ekle",
"NotificationsAppriseSettingsIncludePosterHelpText": "Mesaja poster ekle" "NotificationsAppriseSettingsIncludePosterHelpText": "Mesaja poster ekle",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Dizilere Etiket Ekle",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "İndirme istemcisine (qBittorrent 4.1.0+) eklenen yeni torrentlere dizi etiketleri ekle",
"UserRejectedExtensionsTextsExamples": "Örneğin: '.ext, .xyz' veya 'ext,xyz'",
"MonitorEpisodes": "Bölümleri Takip Et",
"MonitorEpisodesModalInfo": "Bu ayar, bir dizide hangi bölüm veya sezonların takip edileceğini kontrol eder. \"Hiçbiri\" seçilirse, dizi takip edilmeyecektir",
"UserRejectedExtensions": "Ek Olarak Reddedilen Dosya Uzantıları",
"UserRejectedExtensionsHelpText": "Başarısız sayılacak dosya uzantılarını virgülle ayırarak girin (Ayrıca, her dizinleyici için \"İndirmeleri Başarısız Say\" seçeneği etkin olmalıdır)",
"EpisodeMonitoring": "Bölüm Takibi"
} }
@@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.MediaFiles
private readonly IImportApprovedEpisodes _importApprovedEpisodes; private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly IDetectSample _detectSample; private readonly IDetectSample _detectSample;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
public DownloadedEpisodesImportService(IDiskProvider diskProvider, public DownloadedEpisodesImportService(IDiskProvider diskProvider,
@@ -41,6 +43,7 @@ namespace NzbDrone.Core.MediaFiles
IImportApprovedEpisodes importApprovedEpisodes, IImportApprovedEpisodes importApprovedEpisodes,
IDetectSample detectSample, IDetectSample detectSample,
IRuntimeInfo runtimeInfo, IRuntimeInfo runtimeInfo,
IConfigService configService,
Logger logger) Logger logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
@@ -51,6 +54,7 @@ namespace NzbDrone.Core.MediaFiles
_importApprovedEpisodes = importApprovedEpisodes; _importApprovedEpisodes = importApprovedEpisodes;
_detectSample = detectSample; _detectSample = detectSample;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_configService = configService;
_logger = logger; _logger = logger;
} }
@@ -280,6 +284,27 @@ namespace NzbDrone.Core.MediaFiles
}; };
} }
if (_configService.UserRejectedExtensions is not null)
{
var userRejectedExtensions = _configService.UserRejectedExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim(' ', '.')
.Insert(0, "."))
.ToList();
if (userRejectedExtensions.Contains(extension))
{
return new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode
{
Path = fileInfo.FullName
},
new ImportRejection(ImportRejectionReason.UserRejectedExtension, $"Caution: Found file with user defined rejected extension: '{extension}'")),
$"Caution: Found executable file with user defined rejected extension: '{extension}'")
};
}
}
if (extension.IsNullOrWhiteSpace() || !MediaFileExtensions.Extensions.Contains(extension)) if (extension.IsNullOrWhiteSpace() || !MediaFileExtensions.Extensions.Contains(extension))
{ {
_logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension); _logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension);
@@ -7,6 +7,7 @@ public enum ImportRejectionReason
UnknownSeries, UnknownSeries,
DangerousFile, DangerousFile,
ExecutableFile, ExecutableFile,
UserRejectedExtension,
ArchiveFile, ArchiveFile,
SeriesFolder, SeriesFolder,
InvalidFilePath, InvalidFilePath,
@@ -156,7 +156,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem; var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem;
var episodes = _episodeService.GetEpisodes(episodeIds); var episodes = _episodeService.GetEpisodes(episodeIds);
var finalReleaseGroup = releaseGroup.IsNullOrWhiteSpace() var finalReleaseGroup = releaseGroup.IsNullOrWhiteSpace()
? Parser.Parser.ParseReleaseGroup(path) ? Parser.ReleaseGroupParser.ParseReleaseGroup(path)
: releaseGroup; : releaseGroup;
var finalQuality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; var finalQuality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality;
var finalLanguges = var finalLanguges =
@@ -218,7 +218,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
SceneSource = SceneSource(series, rootFolder), SceneSource = SceneSource(series, rootFolder),
ExistingFile = series.Path.IsParentPath(path), ExistingFile = series.Path.IsParentPath(path),
Size = _diskProvider.GetFileSize(path), Size = _diskProvider.GetFileSize(path),
ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.ReleaseGroupParser.ParseReleaseGroup(path) : releaseGroup,
Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages, Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages,
Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality, Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality,
IndexerFlags = (IndexerFlags)indexerFlags, IndexerFlags = (IndexerFlags)indexerFlags,
@@ -331,7 +331,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{ {
var localEpisode = new LocalEpisode(); var localEpisode = new LocalEpisode();
localEpisode.Path = file; localEpisode.Path = file;
localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file); localEpisode.ReleaseGroup = Parser.ReleaseGroupParser.ParseReleaseGroup(file);
localEpisode.Quality = QualityParser.ParseQuality(file); localEpisode.Quality = QualityParser.ParseQuality(file);
localEpisode.Languages = LanguageParser.ParseLanguages(file); localEpisode.Languages = LanguageParser.ParseLanguages(file);
localEpisode.Size = _diskProvider.GetFileSize(file); localEpisode.Size = _diskProvider.GetFileSize(file);
@@ -483,6 +483,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
var imported = new List<ImportResult>(); var imported = new List<ImportResult>();
var importedTrackedDownload = new List<ManuallyImportedFile>(); var importedTrackedDownload = new List<ManuallyImportedFile>();
var importedUntrackedDownload = new List<ImportResult>();
for (var i = 0; i < message.Files.Count; i++) for (var i = 0; i < message.Files.Count; i++)
{ {
@@ -545,7 +546,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
if (trackedDownload == null) if (trackedDownload == null)
{ {
imported.AddRange(_importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode)); var importResult = _importApprovedEpisodes.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode);
imported.AddRange(importResult);
importedUntrackedDownload.AddRange(importResult);
} }
else else
{ {
@@ -566,7 +570,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
_logger.ProgressTrace("Manually imported {0} files", imported.Count); _logger.ProgressTrace("Manually imported {0} files", imported.Count);
} }
var untrackedImports = imported.Where(i => i.Result == ImportResultType.Imported && importedTrackedDownload.FirstOrDefault(t => t.ImportResult != i) == null).ToList(); var untrackedImports = importedUntrackedDownload.Where(i => i.Result == ImportResultType.Imported).ToList();
if (untrackedImports.Any()) if (untrackedImports.Any())
{ {
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (!otherVideoFiles && downloadClientInfo != null && !downloadClientInfo.FullSeason) if (!otherVideoFiles && downloadClientInfo != null && !downloadClientInfo.FullSeason)
{ {
return Parser.Parser.RemoveFileExtension(downloadClientInfo.ReleaseTitle); return FileExtensions.RemoveFileExtension(downloadClientInfo.ReleaseTitle);
} }
var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath()); var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath());
+29 -9
View File
@@ -1,11 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace NzbDrone.Core.MediaFiles namespace NzbDrone.Core.MediaFiles
{ {
internal static class FileExtensions public static class FileExtensions
{ {
private static List<string> _archiveExtensions = new List<string> private static readonly Regex FileExtensionRegex = new(@"\.[a-z0-9]{2,4}$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly HashSet<string> UsenetExtensions = new HashSet<string>()
{
".par2",
".nzb"
};
public static HashSet<string> ArchiveExtensions => new(StringComparer.OrdinalIgnoreCase)
{ {
".7z", ".7z",
".bz2", ".bz2",
@@ -20,8 +30,7 @@ namespace NzbDrone.Core.MediaFiles
".tgz", ".tgz",
".zip" ".zip"
}; };
public static HashSet<string> DangerousExtensions => new(StringComparer.OrdinalIgnoreCase)
private static List<string> _dangerousExtensions = new List<string>
{ {
".arj", ".arj",
".lnk", ".lnk",
@@ -31,8 +40,7 @@ namespace NzbDrone.Core.MediaFiles
".vbs", ".vbs",
".zipx" ".zipx"
}; };
public static HashSet<string> ExecutableExtensions => new(StringComparer.OrdinalIgnoreCase)
private static List<string> _executableExtensions = new List<string>
{ {
".bat", ".bat",
".cmd", ".cmd",
@@ -40,8 +48,20 @@ namespace NzbDrone.Core.MediaFiles
".sh" ".sh"
}; };
public static HashSet<string> ArchiveExtensions => new HashSet<string>(_archiveExtensions, StringComparer.OrdinalIgnoreCase); public static string RemoveFileExtension(string title)
public static HashSet<string> DangerousExtensions => new HashSet<string>(_dangerousExtensions, StringComparer.OrdinalIgnoreCase); {
public static HashSet<string> ExecutableExtensions => new HashSet<string>(_executableExtensions, StringComparer.OrdinalIgnoreCase); title = FileExtensionRegex.Replace(title, m =>
{
var extension = m.Value.ToLower();
if (MediaFileExtensions.Extensions.Contains(extension) || UsenetExtensions.Contains(extension))
{
return string.Empty;
}
return m.Value;
});
return title;
}
} }
} }
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.MediaFiles
public interface IMediaFileRepository : IBasicRepository<EpisodeFile> public interface IMediaFileRepository : IBasicRepository<EpisodeFile>
{ {
List<EpisodeFile> GetFilesBySeries(int seriesId); List<EpisodeFile> GetFilesBySeries(int seriesId);
List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber); List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<EpisodeFile> GetFilesWithoutMediaInfo(); List<EpisodeFile> GetFilesWithoutMediaInfo();
List<EpisodeFile> GetFilesWithRelativePath(int seriesId, string relativePath); List<EpisodeFile> GetFilesWithRelativePath(int seriesId, string relativePath);
@@ -26,6 +27,11 @@ namespace NzbDrone.Core.MediaFiles
return Query(c => c.SeriesId == seriesId).ToList(); return Query(c => c.SeriesId == seriesId).ToList();
} }
public List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds)
{
return Query(c => seriesIds.Contains(c.SeriesId)).ToList();
}
public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber) public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber)
{ {
return Query(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber).ToList(); return Query(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber).ToList();
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
void Update(List<EpisodeFile> episodeFiles); void Update(List<EpisodeFile> episodeFiles);
void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason);
List<EpisodeFile> GetFilesBySeries(int seriesId); List<EpisodeFile> GetFilesBySeries(int seriesId);
List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber); List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<EpisodeFile> GetFiles(IEnumerable<int> ids); List<EpisodeFile> GetFiles(IEnumerable<int> ids);
List<EpisodeFile> GetFilesWithoutMediaInfo(); List<EpisodeFile> GetFilesWithoutMediaInfo();
@@ -71,6 +72,11 @@ namespace NzbDrone.Core.MediaFiles
return _mediaFileRepository.GetFilesBySeries(seriesId); return _mediaFileRepository.GetFilesBySeries(seriesId);
} }
public List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds)
{
return _mediaFileRepository.GetFilesBySeriesIds(seriesIds);
}
public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber) public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber)
{ {
return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber);
@@ -210,7 +210,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
if (videoFormat == "mpeg4" || videoFormat.Contains("msmpeg4")) if (videoFormat == "mpeg4" || videoFormat.Contains("msmpeg4"))
{ {
if (videoCodecID == "XVID") if (videoCodecID.ToUpperInvariant() == "XVID")
{ {
return "XviD"; return "XviD";
} }
@@ -293,7 +293,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
private static string GetSceneNameMatch(string sceneName, params string[] tokens) private static string GetSceneNameMatch(string sceneName, params string[] tokens)
{ {
sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty; sceneName = sceneName.IsNotNullOrWhiteSpace() ? FileExtensions.RemoveFileExtension(sceneName) : string.Empty;
foreach (var token in tokens) foreach (var token in tokens)
{ {
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles
{ {
List<RenameEpisodeFilePreview> GetRenamePreviews(int seriesId); List<RenameEpisodeFilePreview> GetRenamePreviews(int seriesId);
List<RenameEpisodeFilePreview> GetRenamePreviews(int seriesId, int seasonNumber); List<RenameEpisodeFilePreview> GetRenamePreviews(int seriesId, int seasonNumber);
List<RenameEpisodeFilePreview> GetRenamePreviews(List<int> seriesIds);
} }
public class RenameEpisodeFileService : IRenameEpisodeFileService, public class RenameEpisodeFileService : IRenameEpisodeFileService,
@@ -75,6 +76,25 @@ namespace NzbDrone.Core.MediaFiles
.OrderByDescending(e => e.EpisodeNumbers.First()).ToList(); .OrderByDescending(e => e.EpisodeNumbers.First()).ToList();
} }
public List<RenameEpisodeFilePreview> GetRenamePreviews(List<int> seriesIds)
{
var seriesList = _seriesService.GetSeries(seriesIds);
var episodesList = _episodeService.GetEpisodesBySeries(seriesIds).ToLookup(e => e.SeriesId);
var filesList = _mediaFileService.GetFilesBySeriesIds(seriesIds).ToLookup(f => f.SeriesId);
return seriesList.SelectMany(series =>
{
var episodes = episodesList[series.Id].ToList();
var files = filesList[series.Id].ToList();
return GetPreviews(series, episodes, files);
})
.OrderByDescending(e => e.SeriesId)
.ThenByDescending(e => e.SeasonNumber)
.ThenByDescending(e => e.EpisodeNumbers.First())
.ToList();
}
private IEnumerable<RenameEpisodeFilePreview> GetPreviews(Series series, List<Episode> episodes, List<EpisodeFile> files) private IEnumerable<RenameEpisodeFilePreview> GetPreviews(Series series, List<Episode> episodes, List<EpisodeFile> files)
{ {
foreach (var f in files) foreach (var f in files)
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.TorrentInfo
{ {
try try
{ {
return Torrent.Load(fileContents).InfoHash.ToHex(); return Torrent.Load(fileContents).InfoHashes.V1OrV2.ToHex();
} }
catch catch
{ {
@@ -136,6 +136,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodeFile.Episodes.Value.Select(e => e.AirDateUtc))); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodeFile.Episodes.Value.Select(e => e.AirDateUtc)));
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeTitles", string.Join("|", episodeFile.Episodes.Value.Select(e => e.Title))); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeTitles", string.Join("|", episodeFile.Episodes.Value.Select(e => e.Title)));
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeOverviews", string.Join("|", episodeFile.Episodes.Value.Select(e => e.Overview))); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeOverviews", string.Join("|", episodeFile.Episodes.Value.Select(e => e.Overview)));
environmentVariables.Add("Sonarr_EpisodeFile_FinaleTypes", string.Join("|", episodeFile.Episodes.Value.Select(e => e.FinaleType)));
environmentVariables.Add("Sonarr_EpisodeFile_Quality", episodeFile.Quality.Quality.Name); environmentVariables.Add("Sonarr_EpisodeFile_Quality", episodeFile.Quality.Quality.Name);
environmentVariables.Add("Sonarr_EpisodeFile_QualityVersion", episodeFile.Quality.Revision.Version.ToString()); environmentVariables.Add("Sonarr_EpisodeFile_QualityVersion", episodeFile.Quality.Revision.Version.ToString());
environmentVariables.Add("Sonarr_EpisodeFile_ReleaseGroup", episodeFile.ReleaseGroup ?? string.Empty); environmentVariables.Add("Sonarr_EpisodeFile_ReleaseGroup", episodeFile.ReleaseGroup ?? string.Empty);
@@ -207,6 +208,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodes.Select(e => e.AirDateUtc))); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodes.Select(e => e.AirDateUtc)));
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeTitles", string.Join("|", episodes.Select(e => e.Title))); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeTitles", string.Join("|", episodes.Select(e => e.Title)));
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeOverviews", string.Join("|", episodes.Select(e => e.Overview))); environmentVariables.Add("Sonarr_EpisodeFile_EpisodeOverviews", string.Join("|", episodes.Select(e => e.Overview)));
environmentVariables.Add("Sonarr_EpisodeFile_FinaleTypes", string.Join("|", episodes.Select(e => e.FinaleType)));
environmentVariables.Add("Sonarr_EpisodeFile_Qualities", string.Join("|", episodeFiles.Select(f => f.Quality.Quality.Name))); environmentVariables.Add("Sonarr_EpisodeFile_Qualities", string.Join("|", episodeFiles.Select(f => f.Quality.Quality.Name)));
environmentVariables.Add("Sonarr_EpisodeFile_QualityVersions", string.Join("|", episodeFiles.Select(f => f.Quality.Revision.Version))); environmentVariables.Add("Sonarr_EpisodeFile_QualityVersions", string.Join("|", episodeFiles.Select(f => f.Quality.Revision.Version)));
environmentVariables.Add("Sonarr_EpisodeFile_ReleaseGroups", string.Join("|", episodeFiles.Select(f => f.ReleaseGroup))); environmentVariables.Add("Sonarr_EpisodeFile_ReleaseGroups", string.Join("|", episodeFiles.Select(f => f.ReleaseGroup)));
@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using NLog; using NLog;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
@@ -20,6 +19,16 @@ namespace NzbDrone.Core.Notifications.Emby
_logger = logger; _logger = logger;
} }
public void TestConnection(MediaBrowserSettings settings)
{
var path = "/System/Configuration";
var request = BuildRequest(path, settings);
request.Headers.Add("X-MediaBrowser-Token", settings.ApiKey);
var response = _httpClient.Get(request);
_logger.Trace("Response: {0}", response.Content);
}
public void Notify(MediaBrowserSettings settings, string title, string message) public void Notify(MediaBrowserSettings settings, string title, string message)
{ {
var path = "/Notifications/Admin"; var path = "/Notifications/Admin";
@@ -34,21 +43,7 @@ namespace NzbDrone.Core.Notifications.Emby
ImageUrl = "https://raw.github.com/Sonarr/Sonarr/develop/Logo/64.png" ImageUrl = "https://raw.github.com/Sonarr/Sonarr/develop/Logo/64.png"
}.ToJson()); }.ToJson());
try ProcessRequest(request, settings);
{
ProcessRequest(request, settings);
}
catch (HttpException e)
{
if (e.Response.StatusCode == HttpStatusCode.NotFound)
{
_logger.Warn("Unable to send notification to Emby. If you're using Jellyfin disable 'Send Notifications'");
}
else
{
throw;
}
}
} }
public HashSet<string> GetPaths(MediaBrowserSettings settings, Series series) public HashSet<string> GetPaths(MediaBrowserSettings settings, Series series)
@@ -62,8 +62,7 @@ namespace NzbDrone.Core.Notifications.Emby
try try
{ {
_logger.Debug("Testing connection to Emby/Jellyfin : {0}", settings.Address); _logger.Debug("Testing connection to Emby/Jellyfin : {0}", settings.Address);
_proxy.TestConnection(settings);
Notify(settings, "Test from Sonarr", "Success! MediaBrowser has been successfully configured!");
} }
catch (HttpException ex) catch (HttpException ex)
{ {
@@ -71,6 +70,11 @@ namespace NzbDrone.Core.Notifications.Emby
{ {
return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("NotificationsValidationInvalidApiKey")); return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("NotificationsValidationInvalidApiKey"));
} }
else
{
_logger.Trace(ex, "Error when connecting to Emby/Jellyfin");
return new ValidationFailure("Host", _localizationService.GetLocalizedString("NotificationsValidationUnableToSendTestMessage", new Dictionary<string, object> { { "exceptionMessage", ex.Message } }));
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -78,6 +82,25 @@ namespace NzbDrone.Core.Notifications.Emby
return new ValidationFailure("Host", _localizationService.GetLocalizedString("NotificationsValidationUnableToSendTestMessage", new Dictionary<string, object> { { "exceptionMessage", ex.Message } })); return new ValidationFailure("Host", _localizationService.GetLocalizedString("NotificationsValidationUnableToSendTestMessage", new Dictionary<string, object> { { "exceptionMessage", ex.Message } }));
} }
if (settings.Notify)
{
try
{
Notify(settings, "Test from Sonarr", "Success! MediaBrowser has been successfully configured!");
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.NotFound)
{
return new ValidationFailure("Notify", "Unable to send notification to Emby. If you're using Jellyfin disable 'Send Notifications'");
}
else
{
throw;
}
}
}
return null; return null;
} }
} }
@@ -20,6 +20,7 @@ namespace NzbDrone.Core.Notifications.Webhook
AirDateUtc = episode.AirDateUtc; AirDateUtc = episode.AirDateUtc;
SeriesId = episode.SeriesId; SeriesId = episode.SeriesId;
TvdbId = episode.TvdbId; TvdbId = episode.TvdbId;
FinaleType = episode.FinaleType;
} }
public int Id { get; set; } public int Id { get; set; }
@@ -31,5 +32,6 @@ namespace NzbDrone.Core.Notifications.Webhook
public DateTime? AirDateUtc { get; set; } public DateTime? AirDateUtc { get; set; }
public int SeriesId { get; set; } public int SeriesId { get; set; }
public int TvdbId { get; set; } public int TvdbId { get; set; }
public string FinaleType { get; set; }
} }
} }
@@ -15,6 +15,8 @@ namespace NzbDrone.Core.Notifications.Webhook
public int TvMazeId { get; set; } public int TvMazeId { get; set; }
public int TmdbId { get; set; } public int TmdbId { get; set; }
public string ImdbId { get; set; } public string ImdbId { get; set; }
public HashSet<int> MalIds { get; set; }
public HashSet<int> AniListIds { get; set; }
public SeriesTypes Type { get; set; } public SeriesTypes Type { get; set; }
public int Year { get; set; } public int Year { get; set; }
public List<string> Genres { get; set; } public List<string> Genres { get; set; }
@@ -36,6 +38,8 @@ namespace NzbDrone.Core.Notifications.Webhook
TvMazeId = series.TvMazeId; TvMazeId = series.TvMazeId;
TmdbId = series.TmdbId; TmdbId = series.TmdbId;
ImdbId = series.ImdbId; ImdbId = series.ImdbId;
MalIds = series.MalIds;
AniListIds = series.AniListIds;
Type = series.SeriesType; Type = series.SeriesType;
Year = series.Year; Year = series.Year;
Genres = series.Genres; Genres = series.Genres;
+6 -1
View File
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser
new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase) new RegexReplace(@".*?[_. ](S\d{2}(?:E\d{2,4})*[_. ].*)", "$1", RegexOptions.Compiled | RegexOptions.IgnoreCase)
}; };
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>(?:swiss)?german\b|videomann|ger[. ]dub|\bger\b)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_|\b)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH|FRE|FRA)(?:\W|_|\b))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)|(?<urdu>\burdu\b)|(?<romansh>\b(?:romansh|rumantsch|romansch)\b)|(?<original>\b(?:orig|original)\b)", private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>(?:swiss)?german\b|videomann|ger[. ]dub|\bger\b)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_|\b)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH|FRE|FRA)(?:\W|_|\b))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)|(?<urdu>\burdu\b)|(?<romansh>\b(?:romansh|rumantsch|romansch)\b)|(?<japanese>\(JA\))|(?<original>\b(?:orig|original)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b)|(?<german>\bDE\b))(?:(?i)(?![\W|_|^]SUB))", private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b)|(?<german>\bDE\b))(?:(?i)(?![\W|_|^]SUB))",
@@ -496,6 +496,11 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Romansh); languages.Add(Language.Romansh);
} }
if (match.Groups["japanese"].Success)
{
languages.Add(Language.Japanese);
}
if (match.Groups["original"].Success) if (match.Groups["original"].Success)
{ {
languages.Add(Language.Original); languages.Add(Language.Original);
+20 -161
View File
@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@@ -17,45 +18,6 @@ namespace NzbDrone.Core.Parser
{ {
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser)); private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser));
private static readonly RegexReplace[] PreSubstitutionRegex = new[]
{
// Korean series without season number, replace with S01Exxx and remove airdate
new RegexReplace(@"\.E(\d{2,4})\.\d{6}\.(.*-NEXT)$", ".S01E$1.$2", RegexOptions.Compiled),
// Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?:(?<subgroup>[^\]]+?)(?:[\u4E00-\u9FCC]+)?)\]\[(?<title>[^\]]+?)(?:\s(?<chinesetitle>[\u4E00-\u9FCC][^\]]*?))\]\[(?:(?:[\u4E00-\u9FCC]+?)?(?<episode>\d{1,4})(?:[\u4E00-\u9FCC]+?)?)\]", "[${subgroup}] ${title} - ${episode} - ", RegexOptions.Compiled),
// Chinese LoliHouse/ZERO/Lilith-Raws/Skymoon-Raws/orion origin releases don't use the expected brackets, normalize using brackets
new RegexReplace(@"^\[(?<subgroup>[^\]]*?(?:LoliHouse|ZERO|Lilith-Raws|Skymoon-Raws|orion origin)[^\]]*?)\](?<title>[^\[\]]+?)(?: - (?<episode>[0-9-]+)\s*|\[第?(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?\])\[", "[${subgroup}][${title}][${episode}][", RegexOptions.Compiled),
// Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk first and if it has S0x as season number, convert it to Sx
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled),
// Some Chinese releases don't include a separation between Chinese and English titles within the same bracketed group
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\]\[(?<chinesetitle>(?<![^a-zA-Z0-9])[^a-zA-Z0-9]+)(?<title>[^\]]+?)\](?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]{1,4}(?:-[0-9]{1,4})?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title first and if it has S0x as season number, convert it to Sx
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<title>[^\]]+?)(?:\s/\s))(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// GM-Team releases with lots of square brackets
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles separated by | instead of /, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s\|\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Spanish releases with information in brackets
new RegexReplace(@"^(?<title>.+?(?=[ ._-]\()).+?\((?<year>\d{4})\/(?<info>S[^\/]+)", "${title} (${year}) - ${info} ", RegexOptions.Compiled),
};
private static readonly Regex[] ReportTitleRegex = new[] private static readonly Regex[] ReportTitleRegex = new[]
{ {
// Anime - Absolute Episode Number + Title + Season+Episode // Anime - Absolute Episode Number + Title + Season+Episode
@@ -124,7 +86,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title with trailing 3-digit number and sub title - Absolute Episode Number // Anime - [SubGroup] Title with trailing 3-digit number and sub title - Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", new Regex(@"^\[(?<subgroup>[^\]]+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - [SubGroup] Title with trailing number Absolute Episode Number // Anime - [SubGroup] Title with trailing number Absolute Episode Number
@@ -243,6 +205,10 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e|p)(?<seasonpart>\d{1,2}(?!\d+)))+)", new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e|p)(?<seasonpart>\d{1,2}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - 4 digit absolute episode number in batch (1017-1088) or 1017-1088
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?\(?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))[-](?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))\)?",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - 4 digit absolute episode number // Anime - 4 digit absolute episode number
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))", new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
@@ -536,52 +502,18 @@ namespace NzbDrone.Core.Parser
private static readonly Regex PercentRegex = new Regex(@"(?<=\b\d+)%", RegexOptions.Compiled); private static readonly Regex PercentRegex = new Regex(@"(?<=\b\d+)%", RegexOptions.Compiled);
private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(?<![a-f0-9])(8|10)[ -]?(b(?![a-z0-9])|bit))\s*?",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(?<![a-f0-9])(8|10)(b(?![a-z0-9])|bit)|10-bit)\s*?",
string.Empty, string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Valid TLDs http://data.iana.org/TLD/tlds-alpha-by-domain.txt // 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,})[ -]*",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace WebsitePostfixRegex = new RegexReplace(@"(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?:xn--[a-z0-9-]{4,}|[a-z]{2,6})\b(?:\s*\])$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])", private static readonly Regex SixDigitAirDateRegex = new Regex(@"(?<=[_.-])(?<airdate>(?<!\d)(?<airyear>[1-9]\d{1})(?<airmonth>[0-1][0-9])(?<airday>[0-3][0-9]))(?=[_.-])",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace CleanReleaseGroupRegex = new RegexReplace(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen|RePACKPOST))+$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace CleanTorrentSuffixRegex = new RegexReplace(@"\[(?:ettv|rartv|rarbg|cttv|publichd)\]$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex CleanQualityBracketsRegex = new Regex(@"\[[a-z0-9 ._-]+\]$", private static readonly Regex CleanQualityBracketsRegex = new Regex(@"\[[a-z0-9 ._-]+\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+(?<part2>-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?<!(?:WEB-DL|Blu-Ray|480p|576p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES|-ES|-EN|-CAT|-GER|-FRA|-FRE|-ITA|\d{1,2}-bit|[ ._]\d{4}-\d{2}|-\d{2})(?:\k<part2>)?)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[a-z0-9]+)\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex InvalidReleaseGroupRegex = new Regex(@"^([se]\d+|[0-9a-f]{8})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Handle Exception Release Groups that don't follow -RlsGrp; Manual List
// name only...be very careful with this last; high chance of false positives
private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN[_. ]THE[_. ]MEN)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// groups whose releases end with RlsGroup) or RlsGroup]
private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?<releasegroup>(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020|RZeroX|TAoE)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?", private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -700,7 +632,7 @@ namespace NzbDrone.Core.Parser
if (ReversedTitleRegex.IsMatch(title)) if (ReversedTitleRegex.IsMatch(title))
{ {
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension); Array.Reverse(titleWithoutExtension);
title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length)); title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length));
@@ -710,10 +642,9 @@ namespace NzbDrone.Core.Parser
var simpleTitle = title; var simpleTitle = title;
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle); simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = WebsitePostfixRegex.Replace(simpleTitle); simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
return simpleTitle; return simpleTitle;
} }
@@ -731,7 +662,7 @@ namespace NzbDrone.Core.Parser
if (ReversedTitleRegex.IsMatch(title)) if (ReversedTitleRegex.IsMatch(title))
{ {
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension); Array.Reverse(titleWithoutExtension);
title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length)); title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length));
@@ -739,11 +670,11 @@ namespace NzbDrone.Core.Parser
Logger.Debug("Reversed name detected. Converted to '{0}'", title); Logger.Debug("Reversed name detected. Converted to '{0}'", title);
} }
var releaseTitle = RemoveFileExtension(title); var releaseTitle = FileExtensions.RemoveFileExtension(title);
releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]"); releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]");
foreach (var replace in PreSubstitutionRegex) foreach (var replace in ParserCommon.PreSubstitutionRegex)
{ {
if (replace.TryReplace(ref releaseTitle)) if (replace.TryReplace(ref releaseTitle))
{ {
@@ -755,10 +686,9 @@ namespace NzbDrone.Core.Parser
var simpleTitle = SimpleTitleRegex.Replace(releaseTitle); var simpleTitle = SimpleTitleRegex.Replace(releaseTitle);
// TODO: Quick fix stripping [url] - prefixes and postfixes. // TODO: Quick fix stripping [url] - prefixes and postfixes.
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle); simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = WebsitePostfixRegex.Replace(simpleTitle); simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = CleanQualityBracketsRegex.Replace(simpleTitle, m => simpleTitle = CleanQualityBracketsRegex.Replace(simpleTitle, m =>
{ {
@@ -810,7 +740,7 @@ namespace NzbDrone.Core.Parser
result.Quality = QualityParser.ParseQuality(title); result.Quality = QualityParser.ParseQuality(title);
Logger.Debug("Quality parsed: {0}", result.Quality); Logger.Debug("Quality parsed: {0}", result.Quality);
result.ReleaseGroup = ParseReleaseGroup(releaseTitle); result.ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(releaseTitle);
var subGroup = GetSubGroup(match); var subGroup = GetSubGroup(match);
if (!subGroup.IsNullOrWhiteSpace()) if (!subGroup.IsNullOrWhiteSpace())
@@ -930,80 +860,9 @@ namespace NzbDrone.Core.Parser
return null; return null;
} }
public static string ParseReleaseGroup(string title)
{
title = title.Trim();
title = RemoveFileExtension(title);
foreach (var replace in PreSubstitutionRegex)
{
if (replace.TryReplace(ref title))
{
break;
}
}
title = WebsitePrefixRegex.Replace(title);
title = CleanTorrentSuffixRegex.Replace(title);
var animeMatch = AnimeReleaseGroupRegex.Match(title);
if (animeMatch.Success)
{
return animeMatch.Groups["subgroup"].Value;
}
title = CleanReleaseGroupRegex.Replace(title);
var exceptionReleaseGroupRegex = ExceptionReleaseGroupRegex.Matches(title);
if (exceptionReleaseGroupRegex.Count != 0)
{
return exceptionReleaseGroupRegex.OfType<Match>().Last().Groups["releasegroup"].Value;
}
var exceptionExactMatch = ExceptionReleaseGroupRegexExact.Matches(title);
if (exceptionExactMatch.Count != 0)
{
return exceptionExactMatch.OfType<Match>().Last().Groups["releasegroup"].Value;
}
var matches = ReleaseGroupRegex.Matches(title);
if (matches.Count != 0)
{
var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value;
if (int.TryParse(group, out _))
{
return null;
}
if (InvalidReleaseGroupRegex.IsMatch(group))
{
return null;
}
return group;
}
return null;
}
public static string RemoveFileExtension(string title) public static string RemoveFileExtension(string title)
{ {
title = FileExtensionRegex.Replace(title, m => return FileExtensions.RemoveFileExtension(title);
{
var extension = m.Value.ToLower();
if (MediaFiles.MediaFileExtensions.Extensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension))
{
return string.Empty;
}
return m.Value;
});
return title;
} }
public static bool HasMultipleLanguages(string title) public static bool HasMultipleLanguages(string title)
@@ -1309,7 +1168,7 @@ namespace NzbDrone.Core.Parser
return false; return false;
} }
var titleWithoutExtension = RemoveFileExtension(title); var titleWithoutExtension = FileExtensions.RemoveFileExtension(title);
if (RejectHashedReleasesRegexes.Any(v => v.IsMatch(titleWithoutExtension))) if (RejectHashedReleasesRegexes.Any(v => v.IsMatch(titleWithoutExtension)))
{ {
+59
View File
@@ -0,0 +1,59 @@
using System.Text.RegularExpressions;
namespace NzbDrone.Core.Parser;
// These are functions shared between different parser functions
// they are not intended to be used outside of them parsing.
internal static class ParserCommon
{
internal static readonly RegexReplace[] PreSubstitutionRegex = new[]
{
// Korean series without season number, replace with S01Exxx and remove airdate
new RegexReplace(@"\.E(\d{2,4})\.\d{6}\.(.*-NEXT)$", ".S01E$1.$2", RegexOptions.Compiled),
// Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?:(?<subgroup>[^\]]+?)(?:[\u4E00-\u9FCC]+)?)\]\[(?<title>[^\]]+?)(?:\s(?<chinesetitle>[\u4E00-\u9FCC][^\]]*?))\]\[(?:(?:[\u4E00-\u9FCC]+?)?(?<episode>\d{1,4})(?:[\u4E00-\u9FCC]+?)?)\]", "[${subgroup}] ${title} - ${episode} - ", RegexOptions.Compiled),
// Chinese LoliHouse/ZERO/Lilith-Raws/Skymoon-Raws/orion origin releases don't use the expected brackets, normalize using brackets
new RegexReplace(@"^\[(?<subgroup>[^\]]*?(?:LoliHouse|ZERO|Lilith-Raws|Skymoon-Raws|orion origin)[^\]]*?)\](?<title>[^\[\]]+?)(?: - (?<episode>[0-9-]+)\s*|\[第?(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?\])\[", "[${subgroup}][${title}][${episode}][", RegexOptions.Compiled),
// Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk first and if it has S0x as season number, convert it to Sx
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled),
// Some Chinese releases don't include a separation between Chinese and English titles within the same bracketed group
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\]\[(?<chinesetitle>(?<![^a-zA-Z0-9])[^a-zA-Z0-9]+)(?<title>[^\]]+?)\](?:\[\d{4}\])?\[第?(?<episode>[0-9]+(?:-[0-9]+)?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Most Chinese anime releases contain additional brackets/separators for chinese and non-chinese titles, remove junk and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s?★[^\[ -]+\s?)?\[?(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\]\[|\s*[_/·]\s*)){0,2}(?<title>[^\]]+?)\]?(?:\[\d{4}\])?\[第?(?<episode>[0-9]{1,4}(?:-[0-9]{1,4})?)(?:话|集)?(?: ?END|完| ?Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title first and if it has S0x as season number, convert it to Sx
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\[\]]+?)(?:\s(?:S?(?<!\d+)((0)(?<season>\d)|(?<season>[1-9]\d))(?!\d+)))(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?)话?(?:END|完)?", "[${subgroup}] ${title} S${season} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both English and Chinese titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<title>[^\]]+?)(?:\s/\s))(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s/\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// GM-Team releases with lots of square brackets
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:(?<chinesubgroup>\[(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*\])+)\[(?<title>[^\]]+?)\](?<junk>\[[^\]]+\])*\[(?<episode>[0-9]+(?:-[0-9]+)?)( END| Fin)?\]", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Some Chinese anime releases contain both Chinese and English titles separated by | instead of /, remove the Chinese title and replace with normal anime pattern
new RegexReplace(@"^\[(?<subgroup>[^\]]+)\](?:\s)(?:(?<chinesetitle>(?=[^\]]*?[\u4E00-\u9FCC])[^\]]*?)(?:\s\|\s))(?<title>[^\]]+?)(?:[- ]+)(?<episode>[0-9]+(?:-[0-9]+)?(?![a-z]))话?(?:END|完)?", "[${subgroup}] ${title} - ${episode} ", RegexOptions.Compiled),
// Spanish releases with information in brackets
new RegexReplace(@"^(?<title>.+?(?=[ ._-]\()).+?\((?<year>\d{4})\/(?<info>S[^\/]+)", "${title} (${year}) - ${info} ", RegexOptions.Compiled),
};
internal static readonly RegexReplace WebsitePrefixRegex = new(@"^(?:(?:\[|\()\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);
internal static readonly RegexReplace WebsitePostfixRegex = new(@"(?:\[\s*)?(?:www\.)?[-a-z0-9-]{1,256}\.(?:xn--[a-z0-9-]{4,}|[a-z]{2,6})\b(?:\s*\])$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
internal static readonly RegexReplace CleanTorrentSuffixRegex = new(@"\[(?:ettv|rartv|rarbg|cttv|publichd)\]$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
+1 -1
View File
@@ -353,7 +353,7 @@ namespace NzbDrone.Core.Parser
EpisodeNumbers = new int[1] { episode.EpisodeNumber }, EpisodeNumbers = new int[1] { episode.EpisodeNumber },
FullSeason = false, FullSeason = false,
Quality = QualityParser.ParseQuality(releaseTitle), Quality = QualityParser.ParseQuality(releaseTitle),
ReleaseGroup = Parser.ParseReleaseGroup(releaseTitle), ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(releaseTitle),
Languages = LanguageParser.ParseLanguages(releaseTitle), Languages = LanguageParser.ParseLanguages(releaseTitle),
Special = true Special = true
}; };
@@ -0,0 +1,87 @@
using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Core.MediaFiles;
namespace NzbDrone.Core.Parser;
public static class ReleaseGroupParser
{
private static readonly Regex ReleaseGroupRegex = new(@"-(?<releasegroup>[a-z0-9]+(?<part2>-[a-z0-9]+)?(?!.+?(?:480p|576p|720p|1080p|2160p)))(?<!(?:WEB-DL|Blu-Ray|480p|576p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES|-ES|-EN|-CAT|-GER|-FRA|-FRE|-ITA|\d{1,2}-bit|[ ._]\d{4}-\d{2}|-\d{2})(?:\k<part2>)?)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[a-z0-9]+)\]$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex InvalidReleaseGroupRegex = new(@"^([se]\d+|[0-9a-f]{8})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex AnimeReleaseGroupRegex = new(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Handle Exception Release Groups that don't follow -RlsGrp; Manual List
// name only...be very careful with this last; high chance of false positives
private static readonly Regex ExceptionReleaseGroupRegexExact = new(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D|KRaLiMaRKo|BluDragon|DarQ|KCRT|BEN[_. ]THE[_. ]MEN|TAoE|QxR)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
// groups whose releases end with RlsGroup) or RlsGroup]
private static readonly Regex ExceptionReleaseGroupRegex = new(@"(?<=[._ \[])(?<releasegroup>(Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace CleanReleaseGroupRegex = new(@"^(.*?[-._ ](S\d+E\d+)[-._ ])|(-(RP|1|NZBGeek|Obfuscated|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen|RePACKPOST))+$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static string ParseReleaseGroup(string title)
{
title = title.Trim();
title = FileExtensions.RemoveFileExtension(title);
foreach (var replace in ParserCommon.PreSubstitutionRegex)
{
if (replace.TryReplace(ref title))
{
break;
}
}
title = ParserCommon.WebsitePrefixRegex.Replace(title);
title = ParserCommon.CleanTorrentSuffixRegex.Replace(title);
var animeMatch = AnimeReleaseGroupRegex.Match(title);
if (animeMatch.Success)
{
return animeMatch.Groups["subgroup"].Value;
}
title = CleanReleaseGroupRegex.Replace(title);
var exceptionReleaseGroupRegex = ExceptionReleaseGroupRegex.Matches(title);
if (exceptionReleaseGroupRegex.Count != 0)
{
return exceptionReleaseGroupRegex.OfType<Match>().Last().Groups["releasegroup"].Value;
}
var exceptionExactMatch = ExceptionReleaseGroupRegexExact.Matches(title);
if (exceptionExactMatch.Count != 0)
{
return exceptionExactMatch.OfType<Match>().Last().Groups["releasegroup"].Value;
}
var matches = ReleaseGroupRegex.Matches(title);
if (matches.Count != 0)
{
var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value;
if (int.TryParse(group, out _))
{
return null;
}
if (InvalidReleaseGroupRegex.IsMatch(group))
{
return null;
}
return group;
}
return null;
}
}
+2 -1
View File
@@ -4,6 +4,7 @@ using System.Linq;
using NzbDrone.Common.Crypto; using NzbDrone.Common.Crypto;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@@ -65,7 +66,7 @@ namespace NzbDrone.Core.Queue
Episode = episode, Episode = episode,
Languages = trackedDownload.RemoteEpisode?.Languages ?? new List<Language> { Language.Unknown }, Languages = trackedDownload.RemoteEpisode?.Languages ?? new List<Language> { Language.Unknown },
Quality = trackedDownload.RemoteEpisode?.ParsedEpisodeInfo.Quality ?? new QualityModel(Quality.Unknown), Quality = trackedDownload.RemoteEpisode?.ParsedEpisodeInfo.Quality ?? new QualityModel(Quality.Unknown),
Title = Parser.Parser.RemoveFileExtension(trackedDownload.DownloadItem.Title), Title = FileExtensions.RemoveFileExtension(trackedDownload.DownloadItem.Title),
Size = trackedDownload.DownloadItem.TotalSize, Size = trackedDownload.DownloadItem.TotalSize,
SizeLeft = trackedDownload.DownloadItem.RemainingSize, SizeLeft = trackedDownload.DownloadItem.RemainingSize,
TimeLeft = trackedDownload.DownloadItem.RemainingTime, TimeLeft = trackedDownload.DownloadItem.RemainingTime,
+2 -2
View File
@@ -24,10 +24,10 @@
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageReference Include="FluentValidation" Version="9.5.4" /> <PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="MonoTorrent" Version="2.0.7" /> <PackageReference Include="MonoTorrent" Version="3.0.2" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="8.0.5" /> <PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Npgsql" Version="9.0.3" /> <PackageReference Include="Npgsql" Version="9.0.3" />
+6
View File
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Tv
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
Episode FindEpisode(int seriesId, string date, int? part); Episode FindEpisode(int seriesId, string date, int? part);
List<Episode> GetEpisodeBySeries(int seriesId); List<Episode> GetEpisodeBySeries(int seriesId);
List<Episode> GetEpisodesBySeries(List<int> seriesIds);
List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber); List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber);
List<Episode> GetEpisodesBySceneSeason(int seriesId, int sceneSeasonNumber); List<Episode> GetEpisodesBySceneSeason(int seriesId, int sceneSeasonNumber);
List<Episode> EpisodesWithFiles(int seriesId); List<Episode> EpisodesWithFiles(int seriesId);
@@ -99,6 +100,11 @@ namespace NzbDrone.Core.Tv
return _episodeRepository.GetEpisodes(seriesId).ToList(); return _episodeRepository.GetEpisodes(seriesId).ToList();
} }
public List<Episode> GetEpisodesBySeries(List<int> seriesIds)
{
return _episodeRepository.GetEpisodesBySeriesIds(seriesIds).ToList();
}
public List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber) public List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber)
{ {
return _episodeRepository.GetEpisodes(seriesId, seasonNumber); return _episodeRepository.GetEpisodes(seriesId, seasonNumber);
+1 -1
View File
@@ -54,7 +54,7 @@ namespace NzbDrone.Host
b.ClearProviders(); b.ClearProviders();
b.SetMinimumLevel(LogLevel.Trace); b.SetMinimumLevel(LogLevel.Trace);
b.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); b.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
b.AddFilter("Sonarr.Http.Authentication", LogLevel.Information); b.AddFilter("Sonarr.Http.Authentication.ApiKeyAuthenticationHandler", LogLevel.Information);
b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error); b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error);
b.AddNLog(); b.AddNLog();
}); });
@@ -1,6 +1,9 @@
using System;
using System.Linq;
using FluentValidation; using FluentValidation;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using Sonarr.Http; using Sonarr.Http;
@@ -37,6 +40,28 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.ScriptImportPath).IsValidPath().When(c => c.UseScriptImport); SharedValidator.RuleFor(c => c.ScriptImportPath).IsValidPath().When(c => c.UseScriptImport);
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100); SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
SharedValidator.RuleFor(c => c.UserRejectedExtensions).Custom((extensions, context) =>
{
var userRejectedExtensions = extensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => e.Trim(' ', '.')
.Insert(0, "."))
.ToList();
var matchingArchiveExtensions = userRejectedExtensions.Where(ext => FileExtensions.ArchiveExtensions.Contains(ext)).ToList();
if (matchingArchiveExtensions.Count > 0)
{
context.AddFailure($"Rejected extensions may not include valid archive extensions: {string.Join(", ", matchingArchiveExtensions)}");
}
var matchingMediaFileExtensions = userRejectedExtensions.Where(ext => MediaFileExtensions.Extensions.Contains(ext)).ToList();
if (matchingMediaFileExtensions.Count > 0)
{
context.AddFailure($"Rejected extensions may not include valid media file extensions: {string.Join(", ", matchingMediaFileExtensions)}");
}
});
} }
protected override MediaManagementConfigResource ToResource(IConfigService model) protected override MediaManagementConfigResource ToResource(IConfigService model)
@@ -30,6 +30,7 @@ namespace Sonarr.Api.V3.Config
public bool ImportExtraFiles { get; set; } public bool ImportExtraFiles { get; set; }
public string ExtraFileExtensions { get; set; } public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; } public bool EnableMediaInfo { get; set; }
public string UserRejectedExtensions { get; set; }
} }
public static class MediaManagementConfigResourceMapper public static class MediaManagementConfigResourceMapper
@@ -59,7 +60,8 @@ namespace Sonarr.Api.V3.Config
ScriptImportPath = model.ScriptImportPath, ScriptImportPath = model.ScriptImportPath,
ImportExtraFiles = model.ImportExtraFiles, ImportExtraFiles = model.ImportExtraFiles,
ExtraFileExtensions = model.ExtraFileExtensions, ExtraFileExtensions = model.ExtraFileExtensions,
EnableMediaInfo = model.EnableMediaInfo EnableMediaInfo = model.EnableMediaInfo,
UserRejectedExtensions = model.UserRejectedExtensions
}; };
} }
} }
@@ -1,7 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Episodes namespace Sonarr.Api.V3.Episodes
{ {
@@ -26,5 +28,22 @@ namespace Sonarr.Api.V3.Episodes
return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource(); return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource();
} }
[HttpGet("bulk")]
[Produces("application/json")]
public List<RenameEpisodeResource> GetEpisodes([FromQuery] List<int> seriesIds)
{
if (seriesIds is { Count: 0 })
{
throw new BadRequestException("seriesIds must be provided");
}
if (seriesIds.Any(seriesId => seriesId <= 0))
{
throw new BadRequestException("seriesIds must be positive integers");
}
return _renameEpisodeFileService.GetRenamePreviews(seriesIds).ToResource();
}
} }
} }
@@ -8,6 +8,7 @@ using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.CustomFormats; using Sonarr.Api.V3.CustomFormats;
using Sonarr.Api.V3.Episodes; using Sonarr.Api.V3.Episodes;
using Sonarr.Http; using Sonarr.Http;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.ManualImport namespace Sonarr.Api.V3.ManualImport
{ {
@@ -37,6 +38,11 @@ namespace Sonarr.Api.V3.ManualImport
[Consumes("application/json")] [Consumes("application/json")]
public object ReprocessItems([FromBody] List<ManualImportReprocessResource> items) public object ReprocessItems([FromBody] List<ManualImportReprocessResource> items)
{ {
if (items is { Count: 0 })
{
throw new BadRequestException("items must be provided");
}
foreach (var item in items) foreach (var item in items)
{ {
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags, item.ReleaseType); var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags, item.ReleaseType);
@@ -35,6 +35,8 @@ public class SeriesResource : RestResource
public int TvRageId { get; set; } public int TvRageId { get; set; }
public int TvMazeId { get; set; } public int TvMazeId { get; set; }
public int TmdbId { get; set; } public int TmdbId { get; set; }
public HashSet<int>? MalIds { get; set; }
public HashSet<int>? AniListIds { get; set; }
public DateTime? FirstAired { get; set; } public DateTime? FirstAired { get; set; }
public DateTime? LastAired { get; set; } public DateTime? LastAired { get; set; }
public SeriesTypes SeriesType { get; set; } public SeriesTypes SeriesType { get; set; }
@@ -81,6 +83,8 @@ public static class SeriesResourceMapper
TvRageId = model.TvRageId, TvRageId = model.TvRageId,
TvMazeId = model.TvMazeId, TvMazeId = model.TvMazeId,
TmdbId = model.TmdbId, TmdbId = model.TmdbId,
MalIds = model.MalIds,
AniListIds = model.AniListIds,
FirstAired = model.FirstAired, FirstAired = model.FirstAired,
LastAired = model.LastAired, LastAired = model.LastAired,
SeriesType = model.SeriesType, SeriesType = model.SeriesType,
@@ -122,6 +126,8 @@ public static class SeriesResourceMapper
TvRageId = resource.TvRageId, TvRageId = resource.TvRageId,
TvMazeId = resource.TvMazeId, TvMazeId = resource.TvMazeId,
TmdbId = resource.TmdbId, TmdbId = resource.TmdbId,
MalIds = resource.MalIds,
AniListIds = resource.AniListIds,
FirstAired = resource.FirstAired, FirstAired = resource.FirstAired,
SeriesType = resource.SeriesType, SeriesType = resource.SeriesType,
CleanTitle = resource.CleanTitle, CleanTitle = resource.CleanTitle,
@@ -30,7 +30,7 @@ namespace Sonarr.Http.Authentication
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services) public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
{ {
services.AddOptions<CookieAuthenticationOptions>(AuthenticationType.Forms.ToString()) services.AddOptions<CookieAuthenticationOptions>(nameof(AuthenticationType.Forms))
.Configure<IConfigFileProvider>((options, configFileProvider) => .Configure<IConfigFileProvider>((options, configFileProvider) =>
{ {
// Replace diacritics and replace non-word characters to ensure cookie name doesn't contain any valid URL characters not allowed in cookie names // Replace diacritics and replace non-word characters to ensure cookie name doesn't contain any valid URL characters not allowed in cookie names
@@ -47,12 +47,9 @@ namespace Sonarr.Http.Authentication
}); });
return services.AddAuthentication() return services.AddAuthentication()
.AddNone(AuthenticationType.None.ToString()) .AddNone(nameof(AuthenticationType.None))
.AddExternal(AuthenticationType.External.ToString()) .AddExternal(nameof(AuthenticationType.External))
#pragma warning disable CS0618 // Type or member is obsolete .AddCookie(nameof(AuthenticationType.Forms))
.AddCookie(AuthenticationType.Basic.ToString())
#pragma warning restore CS0618 // Type or member is obsolete
.AddCookie(AuthenticationType.Forms.ToString())
.AddApiKey("API", options => .AddApiKey("API", options =>
{ {
options.HeaderName = "X-Api-Key"; options.HeaderName = "X-Api-Key";
@@ -77,7 +77,7 @@ namespace Sonarr.Http.Authentication
private void LogSuccess(HttpRequest context, string username) private void LogSuccess(HttpRequest context, string username)
{ {
_authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username); _authLogger.Debug("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username);
} }
private void LogLogout(HttpRequest context, string username) private void LogLogout(HttpRequest context, string username)
@@ -8,7 +8,7 @@ namespace NzbDrone.Http.Authentication
{ {
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{ {
private const string POLICY_NAME = "UI"; private const string PolicyName = "UI";
private readonly IConfigFileProvider _config; private readonly IConfigFileProvider _config;
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; } public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
@@ -26,7 +26,7 @@ namespace NzbDrone.Http.Authentication
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{ {
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase)) if (policyName.Equals(PolicyName, StringComparison.OrdinalIgnoreCase))
{ {
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString()) var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
.AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement()); .AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement());