1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

40 Commits

Author SHA1 Message Date
Mark McDowall
78913a3e9f New: Subtitles indexer flag to indicate releases with subtitles
Closes #7625
2025-09-01 15:06:44 -07:00
康小广
7fdc4d6638 Follow redirects when fetching Custom Lists 2025-09-01 14:59:34 -07:00
grapexy
309b55fe38 New: Georgian language 2025-09-01 14:58:40 -07:00
oxfordllama
d6f265c7b5 Subtitles indexer flag to indicate BTN releases with subtitles 2025-09-01 14:58:19 -07:00
Weblate
e757dca038 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
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: NanderTGA <nander.roobaert@gmail.com>
Co-authored-by: ReDFiRe <wwsoft@abv.bg>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Xoores <servarr-35466@xoores.cz>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mrchonks <chonkstv@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/
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/nl/
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/tr/
Translation: Servarr/Sonarr
2025-09-01 14:56:39 -07:00
Mark McDowall
9ebe043bd9 New: Move auth success logging to debug
Closes #7978
2025-08-10 21:26:53 -07:00
Mark McDowall
f055e8a3e5 Fixed: Parsing English as the second language in a release name
Closes #8006
2025-08-10 21:26:39 -07:00
Sonarr
8c697afa67 Automated API Docs update
ignore-downstream
2025-08-10 21:20:03 -07:00
Weblate
8d68879edd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translation: Servarr/Sonarr
2025-08-10 21:19:54 -07:00
Trey Turner
e9c82078da Fixed: File air date being updated every refresh
Closes #7989
2025-08-10 21:18:34 -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
112 changed files with 2429 additions and 474 deletions

View File

@@ -7,6 +7,7 @@
### 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.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
#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]"
fi
# Stop the App if running
if service --status-all | grep -Fq "$app"; then
systemctl stop "$app"
systemctl disable "$app".service
echo "Stopped existing $app"
# Stop and disable the App if running
if [ $(systemctl is-active "$app") = "active" ]; then
systemctl disable --now -q "$app"
echo "Stopped and disabled existing $app"
fi
# Create Appdata Directory

View File

@@ -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 FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
@@ -10,6 +12,8 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
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 styles from './RemoveQueueItemModal.css';
@@ -31,12 +35,6 @@ interface RemoveQueueItemModalProps {
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
@@ -49,12 +47,13 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
onModalClose,
} = props;
const dispatch = useDispatch();
const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { removalMethod, blocklistMethod } = useSelector(
(state: AppState) => state.queue.removalOptions
);
const { title, message } = useMemo(() => {
if (!selectedCount) {
@@ -138,18 +137,11 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
return options;
}, [isPending, multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
const handleRemovalOptionInputChange = useCallback(
({ name, value }: InputChanged) => {
dispatch(setQueueRemovalOption({ [name]: value }));
},
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
[dispatch]
);
const handleConfirmRemove = useCallback(() => {
@@ -159,23 +151,11 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
}, [removalMethod, blocklistMethod, onRemovePress]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
}, [onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
@@ -198,7 +178,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalMethodChange}
onChange={handleRemovalOptionInputChange}
/>
</FormGroup>
)}
@@ -216,7 +196,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange}
onChange={handleRemovalOptionInputChange}
/>
</FormGroup>
</ModalBody>

View File

@@ -32,6 +32,17 @@ export interface QueuePagedAppState
removeError: Error;
}
export type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
export type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
interface RemovalOptions {
removalMethod: RemovalMethod;
blocklistMethod: BlocklistMethod;
}
interface QueueAppState {
status: AppSectionItemState<QueueStatus>;
details: QueueDetailsAppState;
@@ -39,6 +50,7 @@ interface QueueAppState {
options: {
includeUnknownSeriesItems: boolean;
};
removalOptions: RemovalOptions;
}
export default QueueAppState;

View File

@@ -83,7 +83,7 @@ function RootFolderRow(props: RootFolderRowProps) {
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('RemoveRootFolder')}
message={translate('RemoveRootFolderMessageText', { path })}
message={translate('RemoveRootFolderWithSeriesMessageText', { path })}
confirmLabel={translate('Remove')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}

View File

@@ -5,7 +5,6 @@
.header {
position: relative;
width: 100%;
height: 425px;
}
.backdrop {
@@ -30,20 +29,18 @@
width: 100%;
height: 100%;
color: var(--white);
gap: 35px;
}
.poster {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 368px;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
width: 100%;
}
.titleRow {
@@ -59,10 +56,13 @@
}
.title {
overflow: auto;
max-height: calc(3 * 50px);
text-wrap: balance;
font-weight: 300;
font-size: 50px;
line-height: 50px;
line-clamp: 3;
}
.toggleMonitoredContainer {
@@ -170,6 +170,8 @@
}
.title {
overflow: hidden;
max-height: calc(3 * 30px);
font-weight: 300;
font-size: 30px;
line-height: 30px;

View File

@@ -1,7 +1,6 @@
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TextTruncate from 'react-text-truncate';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
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 Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import useMeasure from 'Helpers/Hooks/useMeasure';
import usePrevious from 'Helpers/Hooks/usePrevious';
import {
align,
@@ -56,7 +54,6 @@ import {
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import fonts from 'Styles/Variables/fonts';
import sortByProp from 'Utilities/Array/sortByProp';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -75,9 +72,6 @@ import SeriesProgressLabel from './SeriesProgressLabel';
import SeriesTags from './SeriesTags';
import styles from './SeriesDetails.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images: Image[]) {
return images.find((image) => image.coverType === 'fanart')?.url;
}
@@ -246,7 +240,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
allCollapsed: false,
seasons: {},
});
const [overviewRef, { height: overviewHeight }] = useMeasure();
const wasRefreshing = usePrevious(isRefreshing);
const wasRenaming = usePrevious(isRenaming);
@@ -796,16 +789,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
/>
</div>
<div ref={overviewRef} className={styles.overview}>
<TextTruncate
line={
Math.floor(
overviewHeight / (defaultFontSize * lineHeight)
) - 1
}
text={overview}
/>
</div>
<div className={styles.overview}>{overview}</div>
<MetadataAttribution />
</div>

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import React, { SyntheticEvent, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
@@ -122,8 +123,31 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
setIsDeleteSeriesModalOpen(false);
}, [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 linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
@@ -175,7 +199,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
/>
) : null}
<Link className={styles.link} style={elementStyle} to={link}>
<Link className={styles.link} style={elementStyle} {...linkProps}>
<SeriesPoster
style={elementStyle}
images={images}

View File

@@ -361,6 +361,24 @@ function MediaManagement() {
/>
</FormGroup>
) : 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>
) : null}

View File

@@ -55,13 +55,13 @@ function Tag({ id, label }: TagProps) {
}, []);
const handleConfirmDeleteTag = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
const handleDeleteTagModalClose = useCallback(() => {
dispatch(deleteTag({ id }));
}, [id, dispatch]);
const handleDeleteTagModalClose = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
return (
<Card
className={styles.tag}

View File

@@ -31,6 +31,11 @@ export const defaultState = {
includeUnknownSeriesItems: true
},
removalOptions: {
removalMethod: 'removeFromClient',
blocklistMethod: 'doNotBlocklist'
},
status: {
isFetching: false,
isPopulated: false,
@@ -225,6 +230,7 @@ export const defaultState = {
export const persistState = [
'queue.options',
'queue.removalOptions',
'queue.paged.pageSize',
'queue.paged.sortKey',
'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_TABLE_OPTION = 'queue/setQueueTableOption';
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
export const SET_QUEUE_REMOVAL_OPTION = 'queue/setQueueRemoveOption';
export const CLEAR_QUEUE = 'queue/clearQueue';
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 setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
export const setQueueOption = createAction(SET_QUEUE_OPTION);
export const setQueueRemovalOption = createAction(SET_QUEUE_REMOVAL_OPTION);
export const clearQueue = createAction(CLEAR_QUEUE);
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, {
isFetching: false,
isPopulated: false,

View File

@@ -18,5 +18,6 @@ export default interface MediaManagement {
scriptImportPath: string;
importExtraFiles: boolean;
extraFileExtensions: string;
userRejectedExtensions: string;
enableMediaInfo: boolean;
}

View File

@@ -4,6 +4,8 @@ namespace NzbDrone.Common.Extensions
{
public static class DateTimeExtensions
{
public static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static bool InNextDays(this DateTime dateTime, int days)
{
return InNext(dateTime, new TimeSpan(days, 0, 0, 0));
@@ -43,5 +45,10 @@ namespace NzbDrone.Common.Extensions
{
return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond));
}
public static DateTime WithTicksFrom(this DateTime dateTime, DateTime other)
{
return dateTime.WithoutTicks().AddTicks(other.Ticks % TimeSpan.TicksPerSecond);
}
}
}

View File

@@ -390,5 +390,12 @@ namespace NzbDrone.Common.Http
return this;
}
public virtual HttpRequestBuilder AllowRedirect(bool allowAutoRedirect = true)
{
AllowAutoRedirect = allowAutoRedirect;
return this;
}
}
}

View File

@@ -215,6 +215,7 @@ namespace NzbDrone.Common.Instrumentation
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
c.ForLogger("Sonarr.Http.Authentication.ApiKeyAuthenticationHandler").WriteToNil(LogLevel.Info);
});
}

View File

@@ -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; }
}
}

View File

@@ -27,6 +27,7 @@
"TvrageID":"4055",
"ImdbID":"0320037",
"InfoHash":"123",
"Tags": ["Subtitles"],
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"
},
"1234":{
@@ -54,8 +55,9 @@
"TvrageID":"38472",
"ImdbID":"2377081",
"InfoHash":"1234",
"Tags": [],
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=1234&authkey=1234&torrent_pass=1234"
}},
"results":"117927"
}
}
}

View File

@@ -124,5 +124,34 @@
<newznab:attr name="nuked" value="0"/>
</item>
<item>
<title>title</title>
<guid isPermaLink="true">subs=eng</guid>
<link>link</link>
<comments>comments</comments>
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
<category>category</category>
<description>description</description>
<enclosure url="url" length="500" type="application/x-nzb"/>
<newznab:attr name="haspretime" value="0"/>
<newznab:attr name="nuked" value="0"/>
<newznab:attr name="subs" value="Eng"/>
</item>
<item>
<title>title</title>
<guid isPermaLink="true">subs=''</guid>
<link>link</link>
<comments>comments</comments>
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
<category>category</category>
<description>description</description>
<enclosure url="url" length="500" type="application/x-nzb"/>
<newznab:attr name="haspretime" value="0"/>
<newznab:attr name="nuked" value="0"/>
<newznab:attr name="subs" value=""/>
</item>
</channel>
</rss>

View File

@@ -64,6 +64,8 @@ namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests
torrentInfo.Container.Should().Be("MP4");
torrentInfo.Codec.Should().Be("x264");
torrentInfo.Resolution.Should().Be("SD");
torrentInfo.IndexerFlags.Should().HaveFlag(IndexerFlags.Subtitles);
}
private void VerifyBackOff()

View File

@@ -165,6 +165,8 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
[TestCase("nuked=0 attribute")]
[TestCase("prematch=1 and nuked=1 attributes", IndexerFlags.Scene, IndexerFlags.Nuked)]
[TestCase("haspretime=0 and nuked=0 attributes")]
[TestCase("subs=eng", IndexerFlags.Subtitles)]
[TestCase("subs=''")]
public async Task should_parse_indexer_flags(string releaseGuid, params IndexerFlags[] indexerFlags)
{
var feed = ReadAllText(@"Files/Indexers/Newznab/newznab_indexerflags.xml");

View File

@@ -59,6 +59,7 @@ namespace NzbDrone.Core.Test.IndexerTests
public void should_return_season_time_for_season_packs()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal;
settings.SeedCriteria.SeasonPackSeedTime = 10;
Mocker.GetMock<ICachedIndexerSettingsProvider>()
@@ -85,5 +86,71 @@ namespace NzbDrone.Core.Test.IndexerTests
result.Should().NotBeNull();
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);
}
}
}

View File

@@ -60,7 +60,8 @@ namespace NzbDrone.Core.Test.Languages
new object[] { 48, Language.Uzbek },
new object[] { 49, Language.Malay },
new object[] { 50, Language.Urdu },
new object[] { 51, Language.Romansh }
new object[] { 51, Language.Romansh },
new object[] { 52, Language.Georgian }
};
public static object[] ToIntCases =
@@ -115,7 +116,8 @@ namespace NzbDrone.Core.Test.Languages
new object[] { Language.Uzbek, 48 },
new object[] { Language.Malay, 49 },
new object[] { Language.Urdu, 50 },
new object[] { Language.Romansh, 51 }
new object[] { Language.Romansh, 51 },
new object[] { Language.Georgian, 52 }
};
[Test]

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.UpdateEpisodeFileServiceTests
{
[TestFixture]
public class ChangeFileDateForFileFixture : CoreTest<UpdateEpisodeFileService>
{
private readonly DateTime _veryOldAirDateUtc = new(1965, 01, 01, 0, 0, 0, 512, 512, DateTimeKind.Utc);
private DateTime _lastWrite = new(2025, 07, 27, 12, 0, 0, 512, 512, DateTimeKind.Utc);
private Series _series;
private EpisodeFile _episodeFile;
private string _seriesFolder;
private List<Episode> _episodes;
[SetUp]
public void Setup()
{
_seriesFolder = @"C:\Test\TV\Series Title".AsOsAgnostic();
_series = Builder<Series>.CreateNew()
.With(s => s.Path = _seriesFolder)
.Build();
_episodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(e => e.AirDateUtc = _lastWrite.AddDays(2))
.Build()
.ToList();
_episodeFile = Builder<EpisodeFile>.CreateNew()
.With(f => f.Path = Path.Combine(_series.Path, "Season 1", "Series Title - S01E01.mkv").AsOsAgnostic())
.With(f => f.RelativePath = @"Season 1\Series Title - S01E01.mkv".AsOsAgnostic())
.Build();
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.FileGetLastWrite(_episodeFile.Path))
.Returns(() => _lastWrite);
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.FileSetLastWriteTime(_episodeFile.Path, It.IsAny<DateTime>()))
.Callback<string, DateTime>((path, dateTime) =>
{
_lastWrite = dateTime.Kind == DateTimeKind.Utc
? dateTime
: dateTime.ToUniversalTime();
});
Mocker.GetMock<IConfigService>()
.Setup(x => x.FileDate)
.Returns(FileDateType.LocalAirDate);
}
[Test]
public void should_change_date_once_only()
{
var previousWrite = new DateTime(_lastWrite.Ticks, _lastWrite.Kind);
Subject.ChangeFileDateForFile(_episodeFile, _series, _episodes);
Subject.ChangeFileDateForFile(_episodeFile, _series, _episodes);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.FileSetLastWriteTime(_episodeFile.Path, It.IsAny<DateTime>()), Times.Once());
var actualWriteTime = Mocker.GetMock<IDiskProvider>().Object.FileGetLastWrite(_episodeFile.Path).ToLocalTime();
actualWriteTime.Should().Be(_episodes[0].AirDateUtc.Value.ToLocalTime().WithTicksFrom(previousWrite));
}
[Test]
public void should_clamp_mtime_on_posix()
{
PosixOnly();
var previousWrite = new DateTime(_lastWrite.Ticks, _lastWrite.Kind);
_episodes[0].AirDateUtc = _veryOldAirDateUtc;
Subject.ChangeFileDateForFile(_episodeFile, _series, _episodes);
Subject.ChangeFileDateForFile(_episodeFile, _series, _episodes);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.FileSetLastWriteTime(_episodeFile.Path, It.IsAny<DateTime>()), Times.Once());
var actualWriteTime = Mocker.GetMock<IDiskProvider>().Object.FileGetLastWrite(_episodeFile.Path).ToLocalTime();
actualWriteTime.Should().Be(DateTimeExtensions.EpochTime.ToLocalTime().WithTicksFrom(previousWrite));
}
[Test]
public void should_not_clamp_mtime_on_windows()
{
WindowsOnly();
var previousWrite = new DateTime(_lastWrite.Ticks, _lastWrite.Kind);
_episodes[0].AirDateUtc = _veryOldAirDateUtc;
Subject.ChangeFileDateForFile(_episodeFile, _series, _episodes);
Subject.ChangeFileDateForFile(_episodeFile, _series, _episodes);
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.FileSetLastWriteTime(_episodeFile.Path, It.IsAny<DateTime>()), Times.Once());
var actualWriteTime = Mocker.GetMock<IDiskProvider>().Object.FileGetLastWrite(_episodeFile.Path).ToLocalTime();
actualWriteTime.Should().Be(_episodes[0].AirDateUtc.Value.ToLocalTime().WithTicksFrom(previousWrite));
}
}
}

View File

@@ -580,6 +580,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[TestCase("ice", "IS")]
[TestCase("dut", "NL")]
[TestCase("nor", "NO")]
[TestCase("geo", "KA")]
[TestCase("kat", "KA")]
public void should_format_languagecodes_properly(string language, string code)
{
_namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MEDIAINFO.FULL}";

View File

@@ -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! 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("[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)]
public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber)

View File

@@ -69,5 +69,15 @@ namespace NzbDrone.Core.Test.ParserTests
var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Romansh);
}
[TestCase("ka")]
[TestCase("geo")]
[TestCase("kat")]
[TestCase("ka-GE")]
public void should_return_georgian(string isoCode)
{
var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Georgian);
}
}
}

View File

@@ -86,6 +86,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title S01 1080p Eng Fra [mkvonly]")]
[TestCase("Series Title S01 Eng Fre Multi Subs 720p [H264 mp4]")]
[TestCase("Series-Title-S01-[DVDRip]-H264-Fra-Ac3-2-0-Eng-5-1")]
[TestCase("Series Title S01 1080p FR ENG [mkvonly]")]
[TestCase("Series Title S01 1080p ENG FR [mkvonly]")]
public void should_parse_language_french_english(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);
@@ -145,6 +147,7 @@ namespace NzbDrone.Core.Test.ParserTests
}
[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)
{
var result = LanguageParser.ParseLanguages(postTitle);
@@ -526,6 +529,22 @@ namespace NzbDrone.Core.Test.ParserTests
result.Should().Contain(Language.Romansh);
}
[TestCase("Title.the.Series.2025.S01.Georgian.1080p.WEB-DL.h264-RlsGrp")]
[TestCase("Title.the.Series.2025.S01.Geo.1080p.WEB-DL.h264-RlsGrp")]
[TestCase("Title.the.Series.2025.S01.KA.1080p.WEB-DL.h264-RlsGrp")]
public void should_parse_language_georgian(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);
result.Should().Contain(Language.Georgian);
}
[TestCase("Title.the.Series.2025.S01.RU-KA.1080p.WEB-DL.h264-RlsGrp")]
public void should_parse_language_russian_and_georgian(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);
result.Should().BeEquivalentTo(new[] { Language.Russian, Language.Georgian });
}
[TestCase("Name (2020) - S01E20 - [AAC 2.0].testtitle.default.eng.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].eng.default.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]
[TestCase("Name (2020) - S01E20 - [AAC 2.0].default.eng.testtitle.forced.ass", new[] { "default", "forced" }, "testtitle", "English")]

View File

@@ -50,17 +50,17 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title S01 REMUX Dual Audio AVC 1080p 8-Bit-ZR-", "ZR")]
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.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 2.0 afm72) [QxR]", "afm72")]
[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 BluRay x265 HEVC 10bit AAC 2.0 Panda) [QxR]", "Panda")]
[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 WEB-DL x265 HEVC 10bit AC3 5.1 MONOLITH) [QxR]", "MONOLITH")]
[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]", "QxR")]
[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]", "QxR")]
[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]", "QxR")]
[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 (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 - 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("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 DTS-HD MA 2.0 - BluDragon", "BluDragon")]
[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 (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 (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)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().Be(expected);
}
[Test]
@@ -115,7 +136,7 @@ namespace NzbDrone.Core.Test.ParserTests
// [TestCase("", "")]
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")]
@@ -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")]
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")]
@@ -159,13 +180,13 @@ namespace NzbDrone.Core.Test.ParserTests
// [TestCase("", "")]
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]")]
public void should_not_parse_anime_hash_as_release_group(string title)
{
Parser.Parser.ParseReleaseGroup(title).Should().BeNull();
Parser.ReleaseGroupParser.ParseReleaseGroup(title).Should().BeNull();
}
}
}

View File

@@ -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("[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("[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)]
public void should_parse_full_season_release(string postTitle, string title, int season)
{

View File

@@ -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);
}
}
}

View File

@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.ParserTests
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);
}
}
}

View File

@@ -209,9 +209,20 @@ namespace NzbDrone.Core.Configuration
return AuthenticationType.Forms;
}
return Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
var value = Enum.TryParse<AuthenticationType>(_authOptions.Method, out var enumValue)
? enumValue
: 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);
}
#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()

View File

@@ -257,6 +257,12 @@ namespace NzbDrone.Core.Configuration
set { SetValue("EpisodeTitleRequired", value); }
}
public string UserRejectedExtensions
{
get { return GetValue("UserRejectedExtensions", string.Empty); }
set { SetValue("UserRejectedExtensions", value); }
}
public bool SetPermissionsLinux
{
get { return GetValueBoolean("SetPermissionsLinux", false); }

View File

@@ -41,6 +41,7 @@ namespace NzbDrone.Core.Configuration
string ExtraFileExtensions { get; set; }
RescanAfterRefreshType RescanAfterRefresh { get; set; }
EpisodeTitleRequiredType EpisodeTitleRequired { get; set; }
string UserRejectedExtensions { get; set; }
// Permissions (Media Management)
bool SetPermissionsLinux { get; set; }

View File

@@ -12,6 +12,7 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.Datastore.Migration
@@ -809,7 +810,7 @@ namespace NzbDrone.Core.Datastore.Migration
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)
{

View File

@@ -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%'");
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -14,6 +14,7 @@ using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Tags;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.QBittorrent
@@ -22,6 +23,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
{
private readonly IQBittorrentProxySelector _proxySelector;
private readonly ICached<SeedingTimeCacheEntry> _seedingTimeCache;
private readonly ITagRepository _tagRepository;
private class SeedingTimeCacheEntry
{
@@ -38,12 +40,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ICacheManager cacheManager,
ILocalizationService localizationService,
IBlocklistService blocklistService,
ITagRepository tagRepository,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger)
{
_proxySelector = proxySelector;
_seedingTimeCache = cacheManager.GetCache<SeedingTimeCacheEntry>(GetType(), "seedingTime");
_tagRepository = tagRepository;
}
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);
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart)
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart || (Settings.AddSeriesTags && remoteEpisode.Series.Tags.Count > 0))
{
if (!WaitForTorrent(hash))
{
@@ -125,6 +129,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_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;
@@ -140,7 +156,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
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))
{
@@ -182,6 +198,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_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;

View File

@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
void AddTags(string hash, IEnumerable<string> tags, QBittorrentSettings settings);
}
public interface IQBittorrentProxySelector

View File

@@ -273,6 +273,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ProcessRequest(request, settings);
}
public void AddTags(string hash, IEnumerable<string> tags, QBittorrentSettings settings)
{
// Not supported on api v1
}
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)

View File

@@ -338,6 +338,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
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)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)

View File

@@ -73,6 +73,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; }
[FieldDefinition(14, Label = "DownloadClientQbittorrentSettingsAddSeriesTags", Type = FieldType.Checkbox, HelpText = "DownloadClientQbittorrentSettingsAddSeriesTagsHelpText")]
public bool AddSeriesTags { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

View File

@@ -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; }
}
}

View File

@@ -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,
}
}

View File

@@ -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 } }));
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -50,6 +50,12 @@ public class RejectedImportService : IRejectedImportService
_logger.Trace("Download '{0}' contains executable file, marking as failed", trackedDownload.DownloadItem.Title);
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
{
trackedDownload.Warn(new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, importResult.Errors));

View File

@@ -221,7 +221,7 @@ namespace NzbDrone.Core.Download
try
{
hash = MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
hash = MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
}
catch (FormatException ex)
{

View File

@@ -72,7 +72,7 @@ namespace NzbDrone.Core.ImportLists.Custom
}
var baseUrl = settings.BaseUrl.TrimEnd('/');
var request = new HttpRequestBuilder(baseUrl).Accept(HttpAccept.Json).Build();
var request = new HttpRequestBuilder(baseUrl).Accept(HttpAccept.Json).AllowRedirect().Build();
var response = _httpClient.Get(request);
var results = JsonConvert.DeserializeObject<List<TResource>>(response.Content);

View File

@@ -121,6 +121,11 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
break;
}
if (item.Tags?.Contains("Subtitles") == true)
{
flags |= IndexerFlags.Subtitles;
}
return flags;
}

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Indexers.BroadcastheNet
{
public class BroadcastheNetTorrent
@@ -26,6 +28,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
public int? TvrageID { get; set; }
public string ImdbID { get; set; }
public string InfoHash { get; set; }
public List<string> Tags { get; set; }
public string DownloadURL { get; set; }
}
}

View File

@@ -8,5 +8,8 @@ public enum FailDownloads
Executables = 0,
[FieldOption(Label = "Potentially Dangerous")]
PotentiallyDangerous = 1
PotentiallyDangerous = 1,
[FieldOption(Label = "User Defined Extensions")]
UserDefinedExtensions = 2
}

View File

@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
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("NZBFinder.ws", GetSettings("https://nzbfinder.ws", categories: new[] { 5030, 5040, 5045 }));
yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info"));

View File

@@ -210,6 +210,11 @@ namespace NzbDrone.Core.Indexers.Newznab
flags |= IndexerFlags.Nuked;
}
if (TryGetNewznabAttribute(item, "subs").IsNotNullOrWhiteSpace())
{
flags |= IndexerFlags.Subtitles;
}
return flags;
}

View File

@@ -13,10 +13,10 @@ namespace NzbDrone.Core.Indexers.Newznab
{
public class NewznabSettingsValidator : AbstractValidator<NewznabSettings>
{
private static readonly string[] ApiKeyWhiteList =
private static readonly string[] ApiKeyAllowList =
{
"nzbs.org",
"nzb.su",
"nzb.life",
"dognzb.cr",
"nzbplanet.net",
"nzbid.org",
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Indexers.Newznab
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);

View File

@@ -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
}

View File

@@ -49,12 +49,16 @@ namespace NzbDrone.Core.Indexers
return null;
}
var useSeasonPackSeedGoal = (SeasonPackSeedGoal)seedCriteria.SeasonPackSeedGoal == SeasonPackSeedGoal.UseSeasonPackSeedGoal;
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)
{
seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value);

View File

@@ -17,6 +17,10 @@ namespace NzbDrone.Core.Indexers
.When(c => c.SeedTime.HasValue)
.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)
.When(c => c.SeasonPackSeedTime.HasValue)
.AsWarning().WithMessage("Should be greater than zero");
@@ -27,6 +31,11 @@ namespace NzbDrone.Core.Indexers
.When(c => c.SeedRatio > 0.0)
.AsWarning()
.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)
@@ -55,7 +64,13 @@ namespace NzbDrone.Core.Indexers
[FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)]
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; }
}
}

View File

@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Indexers
{
try
{
return MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
return MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
}
catch
{

View File

@@ -12,11 +12,11 @@ namespace NzbDrone.Core.Indexers.Torznab
{
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)
{
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);

View File

@@ -122,6 +122,7 @@ namespace NzbDrone.Core.Languages
public static Language Malay => new Language(49, "Malay");
public static Language Urdu => new Language(50, "Urdu");
public static Language Romansh => new Language(51, "Romansh");
public static Language Georgian => new Language(52, "Georgian");
public static Language Original => new Language(-2, "Original");
public static List<Language> All
@@ -182,6 +183,7 @@ namespace NzbDrone.Core.Languages
Malay,
Urdu,
Romansh,
Georgian,
Original
};
}

View File

@@ -91,7 +91,7 @@
"SeasonDetails": "Детайли за сезона",
"SeasonCount": "Брой сезони",
"SslPort": "SSL порт",
"AppDataLocationHealthCheckMessage": "Актуализирането няма да бъде възможно, за да се предотврати изтриването на папката на приложението по време на актуализацията",
"AppDataLocationHealthCheckMessage": "Актуализацията няма да бъде възможна, за да се предотврати изтриването на AppData при обновяване",
"AppUpdated": "{appName} Актуализиран",
"ApplyTagsHelpTextReplace": "Замяна: Заменете таговете с въведените тагове (не въвеждайте тагове, за да изчистите всички тагове)",
"AudioLanguages": "Аудио езици",
@@ -139,5 +139,53 @@
"StandardEpisodeFormat": "Формат на епизода ( Стандартен )",
"SslCertPathHelpText": "Път до \"pfx\" файл",
"EpisodeNaming": "Именуване на епизоди",
"Close": "Затвори"
"Close": "Затвори",
"AutoTaggingSpecificationMaximumYear": "Максимална година",
"AutoTaggingSpecificationMinimumYear": "Минимална година",
"AutoTaggingSpecificationTag": "Етикет",
"AutoTaggingSpecificationNetwork": "Мрежа(и)",
"Airs": "Излъчва се",
"BranchUpdateMechanism": "Клон, използван от външен механизъм за актуализация",
"BranchUpdate": "Клон, който да се използва за актуализиране на {appName}",
"BindAddressHelpText": "Валиден IP адрес, localhost или '*' за всички интерфейси",
"BlackholeWatchFolder": "Наблюдавана папка",
"BlackholeWatchFolderHelpText": "Папка, от която {appName} трябва да импортира завършените изтегляния",
"BlackholeFolderHelpText": "Папка, в която {appName} ще съхранява файла {extension}",
"BlocklistAndSearchHint": "Започнете търсене на заместител след блокиране",
"BlocklistAndSearchMultipleHint": "Започнете търсене на заместители след блокиране",
"BlocklistAndSearch": "Списък за блокиране и търсене",
"Backups": "Архиви",
"AutoTaggingSpecificationRootFolder": "Основна директория",
"BlocklistFilterHasNoItems": "Избраният филтър за блокиране не съдържа елементи",
"Branch": "Клон",
"BuiltIn": "Вграден",
"BeforeUpdate": "Преди актуализация",
"BlocklistOnly": "Само списък за блокиране",
"BlocklistMultipleOnlyHint": "Списък за блокиране без търсене на заместители",
"BlocklistReleases": "Освобождаване на черния списък",
"BlocklistRelease": "Освобождаване на черния списък",
"Automatic": "Автоматично",
"BackupsLoadError": "Архивите не могат да се заредят",
"BindAddress": "Адрес за обвързване",
"BlocklistLoadError": "Неуспешно зареждане на списъка за блокиране",
"BackupIntervalHelpText": "Интервал между автоматичните резервни копия",
"AutoTaggingSpecificationQualityProfile": "Профил за качество",
"AutoTaggingSpecificationSeriesType": "Тип сериал",
"AutoTaggingSpecificationGenre": "Жанр(ове)",
"AutoTaggingSpecificationOriginalLanguage": "Език",
"AutoTaggingSpecificationStatus": "Статус",
"Backup": "Архивиране",
"BackupFolderHelpText": "Вариант 1: Относителните пътища ще се намират в директорията AppData на {appName}",
"BackupNow": "Архивиране сега",
"AutoTaggingRequiredHelpText": "Условието {implementationName} трябва да съвпада, за да се приложи правилото за автоматично тагване. В противен случай е достатъчно едно съвпадение на {implementationName}.",
"AutomaticAdd": "Автоматично добавяне",
"AutomaticUpdatesDisabledDocker": "Автоматичните актуализации не се поддържат директно при използване на механизма за актуализация на Docker. Ще трябва да актуализирате Image-a на контейнера извън {appName} или да използвате скрипт",
"AutomaticSearch": "Автоматично търсене",
"BackupRetentionHelpText": "Автоматичните резервни копия, по-стари от зададения период на съхранение, ще бъдат изтрити автоматично",
"BypassDelayIfAboveCustomFormatScore": "Пропусни, ако е над рейтинга на персонализирания формат",
"AbsoluteEpisodeNumbers": "Абсолютен номер на епизод(и)",
"BlocklistReleaseHelpText": "Блокира този релийз, така че {appName} да не го изтегля повторно чрез RSS или автоматично търсене",
"Blocklist": "Списък за блокиране",
"BrowserReloadRequired": "Необходимо е презареждане на браузъра",
"BlocklistOnlyHint": "Списък за блокиране без търсене на заместител"
}

View File

@@ -776,7 +776,7 @@
"DownloadClientFreeboxSettingsApiUrl": "URL de l'API",
"DownloadClientSabnzbdValidationEnableDisableTvSorting": "Desactiva l'ordenació de TV",
"MonitorNoEpisodes": "Cap",
"RemoveRootFolder": "Elimina la carpeta arrel",
"RemoveRootFolder": "Elimina la Carpeta Arrel",
"CustomFormatsSpecificationMaximumSize": "Mida màxima",
"DownloadClientFloodSettingsAdditionalTags": "Etiquetes addicionals",
"DownloadClientSettings": "Configuració del client de baixada",
@@ -1505,7 +1505,7 @@
"IndexerSettingsCookieHelpText": "Si el vostre lloc requereix una galeta d'inici de sessió per accedir als rss, haureu de recuperar-la a través d'un navegador.",
"IndexerSettingsMultiLanguageRelease": "Multiidiomes",
"IndexerSettingsFailDownloadsHelpText": "Mentre es processen les descàrregues completades, {appName} tractarà aquests tipus d'arxiu seleccionats com a descàrregues fallides.",
"IndexerSettingsSeasonPackSeedTimeHelpText": "La quantitat de temps que un torrent de pack de temporada s'ha de sembrar abans d'aturar-se, deixeu en blanc per utilitzar el valor per defecte del client de baixada",
"IndexerSettingsSeasonPackSeedTimeHelpText": "La quantitat de temps que un torrent de paquet de temporada s'ha de sembrar abans d'aturar-se, deixeu en blanc per utilitzar el valor per defecte del client de baixada",
"IndexerValidationCloudFlareCaptchaRequired": "Lloc protegit per CloudFlare CAPTCHA. Cal un testimoni CAPTCHA vàlid.",
"IndexerValidationFeedNotSupported": "No s'admet el canal Indexer: {exceptionMessage}",
"IndexerValidationInvalidApiKey": "Clau API no vàlida",
@@ -1689,7 +1689,7 @@
"UpgradeUntilThisQualityIsMetOrExceeded": "Actualitza fins que aquesta qualitat es compleixi o superi",
"UpgradesAllowedHelpText": "Si es desactiven les qualitats no s'actualitzaran",
"IndexerSettingsFailDownloads": "Baixades fallides",
"IndexerSettingsSeasonPackSeedTime": "Hora de la llavor de les packs de temporada",
"IndexerSettingsSeasonPackSeedTime": "Hora de la llavor dels paquets de temporada",
"IndexerValidationNoRssFeedQueryAvailable": "No hi ha disponible cap consulta de fonts RSS. Aquest pot ser un problema amb l'indexador o la configuració de la categoria de l'indexador.",
"LibraryImportTipsSeriesUseRootFolder": "Apunteu {appName} a la carpeta que conté totes les sèries, no una específica. ex. \"`{goodFolderExample}`\" i no eg. \"`{badFolderExample}`\". A més, cada sèrie ha d'estar en la seva pròpia carpeta dins de la carpeta arrel/biblioteca.",
"LocalAirDate": "Data d'emissió local",
@@ -1874,7 +1874,6 @@
"DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "La cua de torrent no està activada a la configuració del qBittorrent. Activeu-lo a qBittorrent o seleccioneu 'Last' com a prioritat.",
"DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent està configurat per a eliminar els torrents quan arribin al límit de la relació de compartició",
"DownloadClientTransmissionSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL rpc de {clientName}, ex. {url}, per defecte a {defaultUrl}",
"DownloadClientUTorrentProviderMessage": "uTorrent té un historial d'inclusió de criptominers, programari maliciós i anuncis, us animem a triar un client diferent.",
"DownloadClientValidationAuthenticationFailureDetail": "Verifiqueu el vostre nom d'usuari i contrasenya. Verifiqueu també si el servidor que executa {appName} no està bloquejat per accedir a {clientName} per les limitacions de WhiteList a la configuració {clientName}.",
"DownloadClientValidationCategoryMissingDetail": "La categoria que heu introduït no existeix a {clientName}. Primer creeu-lo a {clientName}.",
"DownloadClientValidationSslConnectFailure": "No s'ha pogut connectar a través de SSL",
@@ -2161,5 +2160,29 @@
"DownloadClientItemErrorMessage": "{clientName} está reportant un error: {message}",
"EpisodesInSeason": "{episodeCount} episodis en la temporada",
"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+)",
"IndexerSettingsSeasonPackSeedGoal": "Objectiu de compartició per als paquets de temporada",
"IndexerSettingsSeasonPackSeedGoalHelpText": "Trieu si utilitzar diferents objectius de llavor per als paquets de temporada",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Utilitzeu els objectius estàndard",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Utilitzeu els objectius del paquet de temporada",
"IndexerSettingsSeasonPackSeedRatio": "Ràtio de la llavor del paquet de temporada",
"IndexerSettingsSeasonPackSeedRatioHelpText": "La relació a la qual ha d'arribar el torrent d'un paquet de temporada abans d'aturar-se, buida usa el valor predeterminat del client de baixada. La relació ha de ser com a mínim 1.0 i seguir les regles dels indexadors",
"RemoveRootFolderWithSeriesMessageText": "Esteu segur que voleu eliminar la carpeta arrel '{path}'? Els fitxers i carpetes no s'eliminaran del disc, i les sèries d'aquesta carpeta arrel no s'eliminaran de {appName}.",
"DownloadClientTriblerSettingsAnonymityLevel": "Nivell d'anonimat",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Nombre de proxies a usar quan es descarrega contingut. Establir a 0 per a desactivar-ho. Els proxies redueixen la velocitat de descàrrega/pujada. Vegeu {url}",
"DownloadClientTriblerSettingsApiKeyHelpText": "[api].key de triblerd.conf",
"DownloadClientTriblerSettingsDirectoryHelpText": "Ubicació opcional per a desar les baixades, deixeu-ho en blanc per utilitzar la ubicació predeterminada del Tribler",
"DownloadClientTriblerSettingsSafeSeeding": "Compartició segura",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Quan està activat, només es comparteix a través de proxies.",
"DownloadClientTriblerProviderMessage": "La integració tribler és altament experimental. S'ha provat amb {clientName} versió {clientVersionRange}."
}

View File

@@ -178,7 +178,7 @@
"Any": "Jakákoliv",
"ApiKey": "Klíč API",
"AppUpdated": "{appName} aktualizován",
"AppUpdatedVersion": "{appName} byla aktualizována na verzi `{version}`, abyste získali nejnovější změny, musíte znovu načíst {appName}. ",
"AppUpdatedVersion": "{appName} byl aktualizován na verzi `{version}`. Abyste získali nejnovější změny, musíte znovu načíst {appName} ",
"ChooseImportMode": "Vyberte mód importu",
"ClickToChangeEpisode": "Kliknutím změníte epizodu",
"ClickToChangeLanguage": "Kliknutím změníte jazyk",
@@ -316,7 +316,7 @@
"EditSelectedImportLists": "Upravit vybrané seznamy k importu",
"FormatDateTime": "{formattedDate} {formattedTime}",
"AddRootFolderError": "Nepodařilo se přidat kořenový adresář",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {appName}.",
"DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient pro stahování {downloadClientName} je nastaven, aby odstraňoval dokončené stahování. To může vést k tomu, že stažená data budou z klienta odstraněna dříve, než je {appName} bude moci importovat.",
"ConnectionSettingsUrlBaseHelpText": "Přidá předponu do {connectionName} url, jako např. {url}",
"CustomFormatsSpecificationRegularExpressionHelpText": "Vlastní formát RegEx nerozlišuje velká a malá písmena",
"CustomFormatsSpecificationFlag": "Značka",
@@ -357,13 +357,13 @@
"DeleteSpecification": "Smaž specifikace",
"MappedNetworkDrivesWindowsService": "Mapované síťové jednotky nejsou k dispozici, když běží jako služba Windows. Další informace najdete v [FAQ]({url}).",
"DeletedSeriesDescription": "Seriál byl smazán z TheTVDB",
"RecycleBinUnableToWriteHealthCheckMessage": "Nelze zapisovat do nakonfigurované složky koše: {path}. Ujistěte se, že tato cesta existuje a že do ní může zapisovat uživatel se spuštěnou {appName}",
"RecycleBinUnableToWriteHealthCheckMessage": "Nelze zapisovat do nakonfigurované složky koše: {path}. Ujistěte se, že tato cesta existuje a že do ní může zapisovat uživatel, pod kterým běží {appName}",
"DeleteSelectedImportListExclusionsMessageText": "Opravdu smazat vybraný importovaný seznam vyjímek?",
"DoNotUpgradeAutomatically": "Neupgradovat automaticky",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Počáteční stav torrentů přidaných do qBittorrentu. Pamatujte, že vynucené torrenty nedodržují omezení týkající se seedů",
"DeleteSelectedCustomFormats": "Smazat vlastní formát(y)",
"ClickToChangeReleaseType": "Kliknutím změníte typ verze",
"CollapseAll": "Sbal Všechno",
"CollapseAll": "Sbalit vše",
"CutoffUnmetNoItems": "Žádné neodpovídající nesplněné položky",
"CutoffUnmetLoadError": "Chybné načítání nesplněných položek",
"AddDelayProfileError": "Nelze přidat nový profil zpoždění, zkuste to prosím znovu.",
@@ -401,7 +401,7 @@
"DoneEditingGroups": "Úpravy skupin dokončeny",
"DeleteSeriesModalHeader": "Smazat - {title}",
"DeleteSelectedCustomFormatsMessageText": "Opravdu odstranit {count} vybraný vlastní formát(y)?",
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Nedostupný klient pro stahování",
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Nedostupný žádný klient pro stahování",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nelze komunikovat s {downloadClientName}. {errorMessage}",
"DownloadClientFreeboxSettingsAppTokenHelpText": "Token aplikace získaný při vytváření přístupu k Freebox API (tj. app_token)",
"DownloadClientFreeboxSettingsAppToken": "Token aplikace",
@@ -537,7 +537,6 @@
"DownloadClientFreeboxUnableToReachFreebox": "Nelze se připojit k Freebox API. Zkontrolujte nastavení 'Host', 'Port' nebo 'Použít SSL'. (Chyba: {exceptionMessage})",
"IndexerHDBitsSettingsCodecsHelpText": "Pokud nespecifikováno, použijí se všechny možnosti.",
"DownloadClientDownloadStationValidationSharedFolderMissing": "Sdílená složka neexistuje",
"DownloadClientUTorrentProviderMessage": "uTorrent je známý tím, že zahrnuje cryptominery, malware a reklamy, důrazně vám doporučujeme zvolit jiného klienta.",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Přihlaste se do vaší DiskStation jako {username} a ručně to nastavte v nastavení DownloadStation pod BT/HTTP/FTP/NZB -> Umístění.",
"IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexery nedostupné z důvodu selhání delším než 6 hodin: {indexerNames}",
"DownloadClientFloodSettingsRemovalInfo": "{appName} se postará o automatické mazání torrentů podle aktuálních kritérií seedování v Nastavení -> Indexery",
@@ -561,5 +560,19 @@
"Path": "Cesta",
"RegularExpressionsCanBeTested": "Regulární výrazy lze testovat [zde]({url}).",
"NotificationStatusSingleClientHealthCheckMessage": "Oznámení nedostupná z důvodu selhání: {notificationNames}",
"NotificationTriggersHelpText": "Vyber, které události mají vyvolat toto upozornění"
"NotificationTriggersHelpText": "Vyber, které události mají vyvolat toto upozornění",
"MonitorSelected": "Monitorovat vybrané",
"ShowBannersHelpText": "Zobrazit bannery místo jmen",
"ListRootFolderHelpText": "Kořenová složka, do které budou přidány položky seznamu",
"UnmonitorSelected": "Nemonitorovat vybrané",
"ErrorLoadingItem": "Nastala chyba při načítání této položky",
"IndexerJackettAllHealthCheckMessage": "Indexery, které používají nepodporovaný Jackett endpoint 'all': {indexerNames}",
"ReleaseProfileIndexerHelpText": "Výběr, jakých indexerů se profil týká",
"SkipRedownloadHelpText": "Zabraňuje {appName} zkoušet stahovat alternativní vydání pro odebrané položky",
"ErrorLoadingPage": "Nastala chyba při načítání této stránky",
"ExpandAll": "Rozbalit vše",
"ImportListsPlexSettingsAuthenticateWithPlex": "Autentizovat s Plex.tv",
"MissingNoItems": "Žádné chybějící položky",
"RegularExpression": "Regulární výraz",
"IndexerSearchNoInteractiveHealthCheckMessage": "Nejsou dostupné žádné indexery s povoleným interaktivním vyhledáváním, {appName} nemůže poskytnout žádné výsledky interaktivního hledání"
}

View File

@@ -481,6 +481,8 @@
"DownloadClientPneumaticSettingsStrmFolder": "Strm Folder",
"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.",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Add Series Tags",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Add series tags to new torrents added to the download client (qBittorrent 4.1.0+)",
"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+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "First and Last First",
@@ -547,7 +549,13 @@
"DownloadClientStatusSingleClientHealthCheckMessage": "Download clients unavailable due to failures: {downloadClientNames}",
"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}'",
"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",
"DownloadClientUnavailable": "Download Client Unavailable",
"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.",
"IndexerSettingsRssUrl": "RSS URL",
"IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed",
"IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time",
"IndexerSettingsSeasonPackSeedTimeHelpText": "The time a season-pack torrent should be seeded before stopping, empty uses the download client's default",
"IndexerSettingsSeasonPackSeedGoal": "Seeding Goal for Season Packs",
"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",
"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",
@@ -1739,7 +1753,7 @@
"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.",
"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",
"RemoveSelectedBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?",
"RemoveSelectedItem": "Remove Selected Item",
@@ -2140,6 +2154,9 @@
"UsenetDelayTime": "Usenet Delay: {usenetDelay}",
"UsenetDisabled": "Usenet Disabled",
"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",
"UtcAirDate": "UTC Air Date",
"Version": "Version",

View File

@@ -474,7 +474,7 @@
"DelayProfiles": "Perfiles de retraso",
"DeleteCustomFormatMessageText": "¿Estás seguro que quieres eliminar el formato personalizado '{name}'?",
"DeleteBackup": "Eliminar copia de seguridad",
"CopyUsingHardlinksSeriesHelpText": "Los hardlinks permiten a {appName} a importar los torrents que se estén compartiendo a la carpeta de la serie sin usar espacio adicional en el disco o sin copiar el contenido completo del archivo. Los hardlinks solo funcionarán si el origen y el destino están en el mismo volumen",
"CopyUsingHardlinksSeriesHelpText": "Los enlaces físicos permiten a {appName} importar los torrents que se estén sembrando a la carpeta de la serie sin usar espacio adicional en el disco o copiar el contenido completo del archivo. Los enlaces físicos solo funcionarán si el origen y el destino están en el mismo volumen",
"DefaultDelayProfileSeries": "Este es el perfil por defecto. Aplica a todas las series que no tienen un perfil explícito.",
"DelayProfileSeriesTagsHelpText": "Aplica a series con al menos una etiqueta coincidente",
"DeleteCustomFormat": "Eliminar formato personalizado",
@@ -526,7 +526,7 @@
"DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series",
"DoNotPrefer": "No preferir",
"DoNotUpgradeAutomatically": "No actualizar automáticamente",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de parar, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, dejar vacío utiliza el valor predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"Download": "Descargar",
"Donate": "Donar",
"DownloadClientDelugeValidationLabelPluginFailure": "La configuración de etiqueta falló",
@@ -730,7 +730,7 @@
"DownloadFailedEpisodeTooltip": "La descarga del episodio falló",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla",
"DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de sembrado",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una carpeta compartida con el nombre '{sharedFolder}'. ¿Estás seguro que lo has especificado correctamente?",
"EnableCompletedDownloadHandlingHelpText": "Importa automáticamente las descargas completas del gestor de descargas",
@@ -771,7 +771,7 @@
"EpisodeIsNotMonitored": "El episodio no está monitorizado",
"EpisodesLoadError": "No se puede cargar los episodios",
"File": "Archivo",
"HardlinkCopyFiles": "Enlace permanente/Copiar archivos",
"HardlinkCopyFiles": "Enlace físico/Copiar archivos",
"EpisodeDownloaded": "Episodio descargado",
"FileBrowser": "Explorador de archivos",
"FilterDoesNotStartWith": "no empieza con",
@@ -1075,11 +1075,11 @@
"StartupDirectory": "Directorio de Arranque",
"IndexerSettingsAdditionalParametersNyaa": "Parámetros Adicionales",
"IndexerSettingsPasskey": "Clave de acceso",
"IndexerSettingsSeasonPackSeedTime": "Tiempo de Semillado de los Pack de Temporada",
"IndexerSettingsSeasonPackSeedTime": "Tiempo de sembrado de los pack de temporada",
"IndexerSettingsAnimeStandardFormatSearch": "Formato Estándar de Búsqueda de Anime",
"IndexerSettingsAnimeStandardFormatSearchHelpText": "Buscar también anime utilizando la numeración estándar",
"IndexerSettingsApiPathHelpText": "Ruta a la API, usualmente {url}",
"IndexerSettingsSeasonPackSeedTimeHelpText": "La cantidad de tiempo que un torrent de pack de temporada debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga",
"IndexerSettingsSeasonPackSeedTimeHelpText": "El tiempo que un torrent de pack de temporada debe ser sembrado antes de detenerse, si se deja vacío utiliza el valor predeterminado del cliente de descarga",
"IndexerSettingsSeedTime": "Tiempo de sembrado",
"IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores",
"IndexerValidationCloudFlareCaptchaExpired": "El token CAPTCHA de CloudFlare ha caducado, actualícelo.",
@@ -1098,7 +1098,7 @@
"IndexerSettingsRssUrlHelpText": "Introduzca la URL de un canal RSS compatible con {indexer}",
"IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}",
"IndexerHDBitsSettingsMediums": "Medios",
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de parar, vacío usa el predeterminado del cliente de descarga",
"IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser sembrado antes de detenerse, dejar vacío utiliza el valor predeterminado del cliente de descarga",
"IndexerValidationCloudFlareCaptchaRequired": "Sitio protegido por CloudFlare CAPTCHA. Se requiere un token CAPTCHA válido.",
"NotificationsEmailSettingsUseEncryption": "Usar Cifrado",
"LastDuration": "Última Duración",
@@ -1706,7 +1706,7 @@
"UpgradeUntil": "Actualizar hasta",
"UpdaterLogFiles": "Actualizador de archivos de registro",
"UseSeasonFolder": "Usar carpeta de temporada",
"UseHardlinksInsteadOfCopy": "Utilizar enlaces directos en lugar de copiar",
"UseHardlinksInsteadOfCopy": "Utilizar enlaces físicos en lugar de copiar",
"View": "Vista",
"VisitTheWikiForMoreDetails": "Visita la wiki para más detalles: ",
"WaitingToProcess": "Esperar al proceso",
@@ -1875,7 +1875,7 @@
"SmartReplace": "Reemplazo inteligente",
"SupportedDownloadClientsMoreInfo": "Para más información en los clientes de descarga individuales, haz clic en los botones de más información.",
"SupportedImportListsMoreInfo": "Para más información de los listas de importación individuales, haz clic en los botones de más información.",
"TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "En lugar de mover archivos esto indicará a {appName} que copie o enlace (dependiendo de los ajustes/configuración del sistema)",
"TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "En lugar de mover archivos, esto indicará a {appName} que copie o haga un enlace físico (dependiendo de los ajustes/configuración del sistema)",
"TorrentDelay": "Retraso de torrent",
"ToggleMonitoredToUnmonitored": "Monitorizado, haz clic para dejar de monitorizar",
"TorrentBlackholeSaveMagnetFilesHelpText": "Guarda el enlace magnet si no hay ningún archivo .torrent disponible (útil solo si el cliente de descarga soporta magnets guardados en un archivo)",
@@ -1945,7 +1945,7 @@
"NotificationsTwitterSettingsDirectMessageHelpText": "Envía un mensaje directo en lugar de un mensaje público",
"OnApplicationUpdate": "Al actualizar la aplicación",
"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}",
"OverrideGrabNoEpisode": "Al menos un episodio debe ser seleccionado",
"OverrideGrabNoQuality": "La calidad debe ser seleccionada",
@@ -2151,7 +2151,6 @@
"NotificationsTelegramSettingsLinkPreviewHelpText": "Determina qué enlaces se previsualizarán en las notificaciones de Telegram. Elige 'Ninguno' para deshabilitarlo",
"MediaInfoFootNote2": "MediaInfo AudioLanguages excluye el inglés si es el único idioma. Usa MediaInfo AudioLanguagesAll para incluir solo el inglés",
"ReleaseSource": "Fuente de lanzamiento",
"DownloadClientUTorrentProviderMessage": "uTorrent tiene un amplio historial de incluir criptomineros, malware y publicidad, por lo que recomendamos encarecidamente que elijas un cliente diferente.",
"NotificationsPushcutSettingsIncludePoster": "Incluir póster",
"NotificationsPushcutSettingsIncludePosterHelpText": "Incluir póster con notificación",
"NotificationsPushcutSettingsMetadataLinks": "Enlaces de metadatos",
@@ -2163,5 +2162,27 @@
"CloneImportList": "Clonar lista de importación",
"AutoTaggingSpecificationNetwork": "Red(es)",
"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)",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Usar objetivos estándar",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Usar objetivos de pack de temporada",
"IndexerSettingsSeasonPackSeedGoalHelpText": "Elige si usar diferentes objetivos de sembrado para packs de temporada",
"IndexerSettingsSeasonPackSeedRatio": "Ratio de siembra del pack de temporada",
"RemoveRootFolderWithSeriesMessageText": "¿Estás seguro que quieres eliminar la carpeta raíz '{path}'? Los archivos y carpetas no serán borrados del disco, y las series en esta carpeta raíz no serán eliminadas de {appName}.",
"IndexerSettingsSeasonPackSeedGoal": "Objetivo de sembrado para packs de temporada",
"DownloadClientTriblerSettingsApiKeyHelpText": "[api].key de triblerd.conf",
"DownloadClientTriblerSettingsDirectoryHelpText": "Localización opcional en la que poner las descargas, dejar en blanco para usar la localización predeterminada de Tribler",
"IndexerSettingsSeasonPackSeedRatioHelpText": "El ratio que un torrent de pack de temporada debería alcanzar antes de detenerse, si se deja vacío usa el valor predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores",
"DownloadClientTriblerSettingsAnonymityLevel": "Nivel de anonimato",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Número de proxies a usar cuando se descarga contenido. Establecer a 0 para deshabilitarlo. Los proxies reducen la velocidad de descarga/subida. Ver {url}",
"DownloadClientTriblerProviderMessage": "La integración con Tribler es altamente experimental. Probado con {clientName} versión {clientVersionRange}.",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Cuando se habilita, solo se siembra a través de los proxies.",
"DownloadClientTriblerSettingsSafeSeeding": "Sembrado seguro"
}

View File

@@ -816,7 +816,7 @@
"CollapseMultipleEpisodesHelpText": "Tiivistä useat samana päivänä esitettävät jaksot.",
"CalendarLegendSeriesFinaleTooltip": "Sarjan tai kauden päätösjakso",
"CalendarLegendSeriesPremiereTooltip": "Sarjan tai kauden pilottijakso",
"ClickToChangeSeries": "Muuta sarjaa klikkaamalla",
"ClickToChangeSeries": "Vaihda sarja klikkaamalla",
"CloneIndexer": "Monista hakupalvelu",
"Close": "Sulje",
"ClearBlocklist": "Tyhjennä estolista",
@@ -1190,7 +1190,7 @@
"AddedDate": "Lisätty: {date}",
"Anime": "Anime",
"Any": "Mikä tahansa",
"ClickToChangeSeason": "Vaihda tuotantokautta painamalla tästä",
"ClickToChangeSeason": "Vaihda tuotantokausi klikkaamalla",
"CountSelectedFile": "{selectedCount} tiedosto on valittu",
"SingleEpisodeInvalidFormat": "Yksittäinen jakso: virheellinen kaava",
"Underscore": "Alaviiva",
@@ -1242,7 +1242,7 @@
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Vahvista uusi salasana",
"Category": "Kategoria",
"ChownGroup": "chown-ryhmä",
"ClickToChangeEpisode": "Vaihda jaksoa painamalla tästä",
"ClickToChangeEpisode": "Vaihda jakso klikkaamalla",
"CompletedDownloadHandling": "Valmistuneiden latausten käsittely",
"Condition": "Ehto",
"Continuing": "Jatkuu",
@@ -1834,13 +1834,13 @@
"ImportListsMyAnimeListSettingsListStatus": "Listan tila",
"ImportListStatusAllUnavailableHealthCheckMessage": "Mitkään listat eivät ole virheiden vuoksi käytettävissä",
"MetadataKometaDeprecatedSetting": "Poistunut",
"NotificationsTelegramSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.",
"NotificationsTelegramSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit median metatietoihin.",
"OnFileImport": "Kun tiedosto tuodaan",
"OnFileUpgrade": "Kun tiedosto päivitetään",
"ReleaseProfile": "Julkaisuprofiili",
"ShowTags": "Näytä tunnisteet",
"TodayAt": "Tänään klo {time}",
"ClickToChangeReleaseType": "Vaihda julkaisun tyyppiä painamalla tästä",
"ClickToChangeReleaseType": "Vaihda julkaisun tyyppi klikkaamalla",
"CustomFormatsSpecificationSource": "Lähde",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent ilmoittaa puuttuvista tiedostoista",
"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.",
"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.",
"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ä.",
"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.",
@@ -2152,14 +2152,23 @@
"QualityDefinitionsSizeNotice": "Kokorajoitukset on siirretty laatuprofiileihin",
"NotificationsTelegramSettingsLinkPreview": "Linkin esikatselu",
"NotificationsTelegramSettingsLinkPreviewHelpText": "Määrittää minkä linkin esikatselu Telegram-ilmoituksessa näytetään. Poista käytöstä valitsemalla \"Ei mitään\".",
"DownloadClientUTorrentProviderMessage": "Koska uTorrent on tunnettu crypto-, haitta- and mainossisällöstä ja sovelluksista, suosittelemme qBittorrentin, Delugen ja ruTorrentin kaltaisten vaihtoehtojen käyttämistä.",
"NotificationsPushcutSettingsIncludePoster": "Sisällytä juliste",
"NotificationsPushcutSettingsIncludePosterHelpText": "Näytä juliste ilmoituksessa.",
"NotificationsPushcutSettingsMetadataLinks": "Metatietolinkit",
"NotificationsPushcutSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit sarjojen metatietoihin.",
"NotificationsPushcutSettingsMetadataLinksHelpText": "Lisää lähetettäviin ilmoituksiin linkit median metatietoihin.",
"AutoTaggingSpecificationNetwork": "Verkot",
"DownloadClientItemErrorMessage": "{clientName} ilmoittaa virheestä: {message}",
"EpisodesInSeason": "Tuotantokaudessa on {episodeCount} jaksoa",
"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"
}

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.",
"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",
"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}",
"ImportExistingSeries": "Importer une série existante",
"RootFolderSelectFreeSpace": "{freeSpace} Libre",
@@ -677,7 +677,7 @@
"RootFolderMultipleMissingHealthCheckMessage": "Plusieurs dossiers racine sont manquants : {rootFolderPaths}",
"RssIsNotSupportedWithThisIndexer": "RSS n'est pas pris en charge avec cet indexeur",
"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",
"SaveChanges": "Sauvegarder les modifications",
"SceneNumbering": "Numérotation des scènes",
@@ -800,7 +800,7 @@
"UpdaterLogFiles": "Journaux du programme de mise à jour",
"UpgradeUntil": "Mise à niveau jusqu'à",
"UpgradeUntilCustomFormatScore": "Mise à niveau jusqu'au score de format personnalisé",
"UpgradeUntilCustomFormatScoreEpisodeHelpText": "Une fois ce score de format personnalisé atteint, {appName} ne récupérera plus les sorties d'épisodes",
"UpgradeUntilCustomFormatScoreEpisodeHelpText": "Une fois que la qualité minimum est atteinte ou dépassée et que le score de format personnalisé est atteint, {appName} ne récupérera plus les sorties d'épisodes",
"UrlBase": "URL de base",
"UseHardlinksInsteadOfCopy": "Utiliser les liens durs au lieu de copier",
"UseSeasonFolder": "Utiliser le dossier de la saison",
@@ -904,7 +904,7 @@
"UnmappedFilesOnly": "Fichiers non mappés uniquement",
"UnmonitorSpecialsEpisodesDescription": "Annulez la surveillance de tous les épisodes spéciaux sans modifier le statut surveillé des autres épisodes",
"UpdateUiNotWritableHealthCheckMessage": "Impossible d'installer la mise à jour, car le dossier de l'interface utilisateur « {uiFolder} » n'est pas accessible en écriture par l'utilisateur « {userName} ».",
"UpgradeUntilEpisodeHelpText": "Une fois cette qualité atteinte, {appName} ne téléchargera plus d'épisodes",
"UpgradeUntilEpisodeHelpText": "Une fois cette qualité atteinte, {appName} ne téléchargera plus d'épisodes une fois le que le score du format personnalisé est atteint ou dépassé",
"UpgradeUntilThisQualityIsMetOrExceeded": "Mise à niveau jusqu'à ce que cette qualité soit atteinte ou dépassée",
"UseProxy": "Utiliser le proxy",
"WaitingToImport": "En attente d'import",
@@ -1191,7 +1191,7 @@
"PackageVersion": "Version du paquet",
"PackageVersionInfo": "{packageVersion} par {packageAuthor}",
"QuickSearch": "Recherche rapide",
"ReleaseRejected": "Libération rejetée",
"ReleaseRejected": "Version rejetée",
"ReleaseSceneIndicatorAssumingScene": "En supposant la numérotation des scènes.",
"ReleaseSceneIndicatorAssumingTvdb": "En supposant la numérotation TVDB.",
"ReleaseSceneIndicatorMappedNotRequested": "L'épisode mappé n'a pas été demandé dans cette recherche.",
@@ -2017,7 +2017,7 @@
"ImportListsTraktSettingsPopularListTypeTrendingShows": "Spectacles en vogue",
"ImportListsTraktSettingsPopularName": "Liste populaire de Trakt",
"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",
"ImportListsTraktSettingsWatchListSorting": "Tri de la liste de surveillance",
"ImportListsTraktSettingsWatchListSortingHelpText": "Si le type de liste est surveillé, sélectionnez l'ordre de tri de la liste",
@@ -2123,5 +2123,66 @@
"LastSearched": "Dernière recherche",
"FolderNameTokens": "Jetons de nom de dossier",
"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",
"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",
"QualityDefinitionsSizeNotice": "Les restrictions de taille sont maintenant dans les profils de qualité",
"UserInvokedSearch": "Recherche invoquée par lutilisateur",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Nombre de proxy à utiliser pour télécharger. Pour désactiver, mettre à 0. Les proxy réduisent la vitesse d'envoi et de réception. Voir {url}",
"DownloadClientTriblerSettingsDirectoryHelpText": "Emplacement optionnel où mettre les téléchargements, laisser vide pour utiliser l'emplacement Tribler par défaut",
"CloneImportList": "Cloner la liste d'importation",
"IndexerSettingsSeasonPackSeedGoal": "Objective de partage pour les packs de saison",
"IndexerSettingsSeasonPackSeedGoalHelpText": "Sélectionné si des objectifs différent devraient être utilisés pour les packs de saison",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Utiliser les objectifs standards",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Utiliser les objectifs pour les packs de saison",
"IndexerSettingsSeasonPackSeedRatio": "Ratio de partage pour les packs de saison",
"MediaInfoFootNote2": "MediaInfo AudioLanguages exclue langlais sil sagit de la seule langue. Utiliser MediaInfo AudioLanguagesAll pour inclure ceux seulement en anglais",
"NotificationsAppriseSettingsIncludePosterHelpText": "Inclure le poster dans le message",
"ReleasePush": "Délai de version",
"ReleaseSource": "Source de version",
"RemoveRootFolderWithSeriesMessageText": "Êtes-vous sûr de vouloir supprimer le dossier racine '{path}' ? Les fichiers et les dossiers ne seront pas supprimés du disque, et les séries dans ce dossier racine ne seront pas supprimées de {appName}.",
"IndexerSettingsSeasonPackSeedRatioHelpText": "Le ratio quun torrent de pack de saison devrait atteindre avant darrêter. Laisser à vide utilise le default du client de téléchargement. Le ratio devrait être au moins 1.0 et sur les règles de lindexeur",
"DownloadClientTriblerSettingsAnonymityLevel": "Niveau danonymat",
"DownloadClientTriblerSettingsApiKeyHelpText": "[api].key de triblerd.conf",
"DownloadClientTriblerSettingsSafeSeeding": "Partage protégé",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Lorsque activé, seulement partager via un proxy.",
"DownloadClientTriblerProviderMessage": "Lintégration avec tribler est hautement expérimental. Tester sur {clientName} version {clientVersionRange}.",
"NotificationsAppriseSettingsIncludePoster": "Inclure le poster"
}

View File

@@ -58,5 +58,10 @@
"AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime",
"AddConditionError": "Neuspješno dodavanje novog uvjeta, molimo pokušaj ponovno.",
"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"
}

View File

@@ -61,7 +61,7 @@
"AddDelayProfile": "Voeg vertragingsprofiel toe",
"AddCustomFormatError": "Kan geen nieuw aangepast formaat toevoegen. Probeer het opnieuw.",
"AddDownloadClientError": "Kan geen nieuwe downloadclient toevoegen. Probeer het opnieuw.",
"AddDownloadClient": "Download Client Toevoegen",
"AddDownloadClient": "Downloadprogramma Toevoegen",
"AddIndexerError": "Kon geen nieuwe indexeerder toevoegen, Probeer het opnieuw.",
"AddList": "Lijst Toevoegen",
"AddListError": "Kon geen nieuwe lijst toevoegen, probeer het opnieuw.",
@@ -266,5 +266,36 @@
"CertificateValidationHelpText": "Verander hoe strikt HTTPS-certificaatvalidatie is. Verander dit niet als je de risico's niet begrijpt.",
"ChmodFolderHelpText": "Octaal, toegepast tijdens importeren/hernoemen van mediamappen en -bestanden (zonder uitvoeringsbits)",
"CalendarFeed": "{appName} kalenderfeed",
"BypassDelayIfHighestQualityHelpText": "Omzeil uitstel wanneer de uitgave de hoogst mogelijke kwaliteit heeft uit het kwaliteitsprofiel van het geprefereerde protocol"
"BypassDelayIfHighestQualityHelpText": "Omzeil uitstel wanneer de uitgave de hoogst mogelijke kwaliteit heeft uit het kwaliteitsprofiel van het geprefereerde protocol",
"DownloadClientValidationSslConnectFailure": "Kan niet verbinden via SSL",
"DownloadClientValidationGroupMissing": "Groep bestaat niet",
"DownloadClientValidationUnknownException": "Onbekende fout: {exception}",
"DownloadClientValidationVerifySsl": "Verifieer SSL instellingen",
"DownloadClients": "Downloadprogramma's",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Aantal proxies om te gebruiken bij het downloaden van content. Zet dit op 0 om uit te schakelen. Proxies vertragen download- en uploadsnelheid. Zie {url}",
"DownloadClientTriblerProviderMessage": "De Tribler-integratie is heel experimenteel. Getest met {clientName} versie {clientVersionRange}.",
"DownloadClientValidationAuthenticationFailureDetail": "Gelieve uw gebruikersnaam en wachtwoord te verifiëren. Verifieer ook of de host waar {appName} op draait niet geblokkeerd is voor toegang tot {clientName} door Whitelist limitaties in de {clientName} instellingen.",
"DownloadClientValidationSslConnectFailureDetail": "{appName} kan niet verbinden met {clientName} via SSL. Dit probleem kan computergerelateerd zijn. Probeer alstublieft om {appName} en {clientName} te configureren om geen SSL te gebruiken.",
"DownloadClientValidationAuthenticationFailure": "Authenticatiefout",
"DownloadClientTriblerSettingsApiKeyHelpText": "[api].key uit triblerd.conf",
"DownloadClientTriblerSettingsDirectoryHelpText": "Optionele locatie om downloads in te plaatsen, laat leeg om de standaard Tribler locatie te gebruiken",
"DownloadClientTriblerSettingsSafeSeeding": "Veilig Seeden",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Wanneer ingeschakeld alleen maar seeden via proxies.",
"DownloadClientUTorrentTorrentStateError": "uTorrent rapporteert een fout",
"DownloadClientUnavailable": "Download Client niet beschikbaar",
"DownloadClientValidationApiKeyIncorrect": "API-sleutel Incorrect",
"DownloadClientValidationApiKeyRequired": "API-sleutel Nodig",
"DownloadClientValidationCategoryMissing": "Categorie bestaat niet",
"DownloadClientValidationCategoryMissingDetail": "De ingevoerde categorie bestaat niet in {clientName}. Voeg deze eerst toe in {clientName}.",
"DownloadClientValidationErrorVersion": "{clientName} versie moet tenminste {requiredVersion} zijn. Gerapporteerde versie is {reportedVersion}",
"DownloadClientValidationGroupMissingDetail": "De ingevoerde groep bestaat niet in {clientName}. Voeg deze eerst toe in {clientName}.",
"DownloadClientValidationTestNzbs": "Kon de lijst van NZBs niet verkrijgen: {exceptionMessage}",
"DownloadClientValidationTestTorrents": "Kon de lijst van torrents niet verkrijgen: {exceptionMessage}",
"DownloadClientValidationUnableToConnect": "Kon niet verbinden met {clientName}",
"DownloadClientValidationUnableToConnectDetail": "Gelieve de hostnaam en poort te verifiëren.",
"DownloadClientValidationVerifySslDetail": "Gelieve uw SSL-configuratie te verifiëren in zowel {clientName} als {appName}",
"DownloadClientVuzeValidationErrorVersion": "Protocolversie niet ondersteund, gebruik Vuze 5.0.0.0 of hoger met de Vuze Web Remote plugin.",
"DownloadClientsLoadError": "Kan downloadprogramma's niet laden",
"GeneralSettingsLoadError": "Algemene instellingen konden niet worden geladen",
"GeneralSettingsSummary": "Poort, SSL, gebruikersnaam/wachtwoord, proxy, analytics en updates"
}

View File

@@ -55,7 +55,7 @@
"Added": "Adicionado",
"ApiKeyValidationHealthCheckMessage": "Atualize sua chave de API para ter pelo menos {length} caracteres. Você pode fazer isso através das configurações ou do arquivo de configuração",
"RemoveCompletedDownloads": "Remover downloads concluídos",
"AppDataLocationHealthCheckMessage": "Não será possível atualizar para evitar a exclusão de AppData na Atualização",
"AppDataLocationHealthCheckMessage": "A atualização não será possível para impedir a exclusão de AppData na Atualização",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Não é possível se comunicar com {downloadClientName}. {errorMessage}",
"DownloadClientRootFolderHealthCheckMessage": "O cliente de download {downloadClientName} coloca os downloads na pasta raiz {rootFolderPath}. Você não deve baixar para uma pasta raiz.",
"DownloadClientSortingHealthCheckMessage": "O cliente de download {downloadClientName} tem classificação {sortingMode} habilitada para a categoria do {appName}. Você deve desabilitar essa classificação em seu cliente de download para evitar problemas de importação.",
@@ -326,7 +326,7 @@
"Grabbed": "Obtido",
"Ignored": "Ignorado",
"Imported": "Importado",
"IncludeUnmonitored": "Incluir não monitorados",
"IncludeUnmonitored": "Incluir Não Monitorados",
"Indexer": "Indexador",
"LatestSeason": "Temporada mais recente",
"MissingEpisodes": "Episódios ausentes",
@@ -383,7 +383,7 @@
"LastUsed": "Usado por último",
"MoveAutomatically": "Mover automaticamente",
"NoResultsFound": "Nenhum resultado encontrado",
"RemoveRootFolder": "Remover pasta raiz",
"RemoveRootFolder": "Remover Pasta Raiz",
"RemoveTagsAutomatically": "Remover Tags Automaticamente",
"Season": "Temporada",
"SelectFolder": "Selecionar Pasta",
@@ -919,7 +919,7 @@
"VisitTheWikiForMoreDetails": "Visite o wiki para mais detalhes: ",
"WantMoreControlAddACustomFormat": "Quer mais controle sobre quais downloads são preferidos? Adicione um [Formato Personalizado](/settings/customformats)",
"UnmonitorSpecialEpisodes": "Não Monitorar Especiais",
"MonitorAllEpisodes": "Todos os episódios",
"MonitorAllEpisodes": "Todos os Episódios",
"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.",
"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\"",
"Monitor": "Monitorar",
"MonitorAllEpisodesDescription": "Monitorar todos os episódios, exceto os especiais",
"MonitorExistingEpisodes": "Episódios existentes",
"MonitorExistingEpisodes": "Episódios Existentes",
"MonitorFirstSeason": "Primeira temporada",
"MonitorFirstSeasonDescription": "Monitorar todos os episódios da primeira temporada. As demais temporadas serão ignoradas",
"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.",
"DownloadClientFloodSettingsAdditionalTagsHelpText": "Adiciona propriedades de mídia como etiquetas. As dicas são exemplos.",
"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",
"Category": "Categoria",
"Directory": "Diretório",
@@ -1632,7 +1632,7 @@
"IndexerSettingsMinimumSeedersHelpText": "Quantidade mínima de semeadores necessária.",
"IndexerSettingsPasskey": "Chave de acesso",
"IndexerSettingsRssUrl": "URL do RSS",
"IndexerSettingsSeasonPackSeedTime": "Tempo de semeadura para pacotes de temporada",
"IndexerSettingsSeasonPackSeedTime": "Tempo de Semeadura para Pacotes de Temporada",
"IndexerSettingsSeedRatio": "Proporção de semeadura",
"IndexerSettingsSeedTime": "Tempo de semeadura",
"IndexerSettingsSeedTimeHelpText": "Quanto tempo um torrent deve ser semeado antes de parar, deixe vazio para usar o padrão do cliente de download",
@@ -1675,7 +1675,7 @@
"PasswordConfirmation": "Confirmação Da Senha",
"MonitorPilotEpisodeDescription": "Monitorar apenas o primeiro episódio da primeira temporada",
"MonitorNoNewSeasonsDescription": "Não monitorar nenhuma nova temporada automaticamente",
"MonitorAllSeasons": "Todas as temporadas",
"MonitorAllSeasons": "Todas as Temporadas",
"MonitorAllSeasonsDescription": "Monitorar todas as novas temporadas automaticamente",
"MonitorLastSeason": "Última temporada",
"MonitorLastSeasonDescription": "Monitorar todos os episódios da última temporada",
@@ -2019,7 +2019,7 @@
"IgnoreDownload": "Ignorar download",
"ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Todas as listas requerem interação manual devido a possíveis buscas parciais",
"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)",
"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)",
@@ -2032,8 +2032,8 @@
"CustomFormatsSpecificationFlag": "Sinalizador",
"IndexerFlags": "Sinalizadores do indexador",
"SetIndexerFlags": "Definir Sinalizadores de Indexador",
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar monitoramento da temporada",
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronizar o monitoramento da temporada da instância do {appName}. Se ativada, \"Monitorar\" será ignorado",
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar Monitoramento da Temporada",
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincronizar o monitoramento da temporada da instância do {appName}. Se ativada, 'Monitorar' será ignorado",
"CustomFilter": "Filtro personalizado",
"Filters": "Filtros",
"Label": "Rótulo",
@@ -2065,7 +2065,7 @@
"DayOfWeekAt": "{day} às {time}",
"TodayAt": "Hoje às {time}",
"TomorrowAt": "Amanhã às {time}",
"HasUnmonitoredSeason": "Há temporadas não monitoradas",
"HasUnmonitoredSeason": "Há Temporadas Não Monitoradas",
"YesterdayAt": "Ontem às {time}",
"UnableToImportAutomatically": "Não foi possível importar automaticamente",
"CustomColonReplacement": "Personalizar substituto do dois-pontos",
@@ -2152,7 +2152,6 @@
"QualityDefinitionsSizeNotice": "As restrições de tamanho foram transferidas para Perfis de Qualidade",
"NotificationsTelegramSettingsLinkPreview": "Prévia do Link",
"NotificationsTelegramSettingsLinkPreviewHelpText": "Determina qual link será visualizado na notificação do Telegram. Escolha 'Nenhum' para desativar",
"DownloadClientUTorrentProviderMessage": "O uTorrent tem um histórico de incluir criptomineradores, malware e anúncios, recomendamos fortemente que você escolha um cliente diferente.",
"NotificationsPushcutSettingsIncludePoster": "Incluir pôster",
"NotificationsPushcutSettingsMetadataLinks": "Links de metadados",
"NotificationsPushcutSettingsIncludePosterHelpText": "Incluir pôster com notificação",
@@ -2163,5 +2162,27 @@
"EpisodesInSeason": "{episodeCount} episódios na temporada",
"AutoTaggingSpecificationNetwork": "Rede(s)",
"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",
"IndexerSettingsSeasonPackSeedGoal": "Meta de Semeadura para Pacotes de Temporada",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Usar Metas Padrões",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Usar as Metas de Pacote de Temporada",
"IndexerSettingsSeasonPackSeedRatio": "Proporção de Semeadura do Pacote de Temporada",
"IndexerSettingsSeasonPackSeedRatioHelpText": "A proporção que um torrent de pacote de temporada deve atingir antes de parar, vazio usa o padrão do cliente de download. A proporção deve ser de pelo menos 1,0 e seguir as regras dos indexadores",
"IndexerSettingsSeasonPackSeedGoalHelpText": "Escolha se deseja usar metas de semeadura diferentes para pacotes de temporada",
"RemoveRootFolderWithSeriesMessageText": "Tem certeza de que deseja remover a pasta raiz '{path}'? Arquivos e pastas não serão excluídos do disco e as séries nesta pasta raiz não serão removidas de {appName}.",
"DownloadClientTriblerSettingsAnonymityLevel": "Nível de anonimato",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Número de proxies a serem usados ao baixar conteúdo. Para desativar, defina como 0. Os proxies reduzem a velocidade de download/upload. Veja {url}",
"DownloadClientTriblerSettingsApiKeyHelpText": "[API]. chave de triblerd.conf",
"DownloadClientTriblerSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Tribler",
"DownloadClientTriblerSettingsSafeSeeding": "Semeadura Segura",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Quando ativado, apenas semeia por meio de proxies.",
"DownloadClientTriblerProviderMessage": "A integração tribler é altamente experimental. Testado em {clientName} versão {clientVersionRange}."
}

View File

@@ -25,7 +25,7 @@
"Close": "Închide",
"Delete": "Șterge",
"Added": "Adăugat",
"CountSeasons": "{count} sezoane",
"CountSeasons": "{count} Sezoane",
"DownloadClientStatusSingleClientHealthCheckMessage": "Clienții de descărcare indisponibili datorită erorilor: {downloadClientNames}",
"EnableAutomaticSearch": "Activați căutarea automată",
"EnableInteractiveSearch": "Activați căutarea interactivă",
@@ -172,7 +172,7 @@
"HistoryLoadError": "Istoricul nu poate fi încărcat",
"DefaultNameCopiedSpecification": "{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",
"Or": "sau",
"PendingDownloadClientUnavailable": "În așteptare - Clientul de descărcare nu este disponibil",
@@ -212,5 +212,17 @@
"Clone": "Clonează",
"DownloadClientSettingsOlderPriority": "Prioritate mai vechi",
"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ă"
}

View File

@@ -269,7 +269,7 @@
"AnimeEpisodeFormat": "Формат аниме-эпизода",
"AuthBasic": "Базовый (Всплывающее окно браузера)",
"AuthForm": "Формы (Страница авторизации)",
"Authentication": "Авторизация",
"Authentication": "Аутентификация",
"AuthenticationRequired": "Требуется авторизация",
"BackupIntervalHelpText": "Периодичность автоматического резервного копирования",
"BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены",
@@ -1371,7 +1371,7 @@
"Hostname": "Имя хоста",
"Host": "Хост",
"IndexerSettingsRssUrlHelpText": "Введите URL-адрес RSS-канала, совместимого с {indexer}",
"IndexerSettingsSeasonPackSeedTimeHelpText": "Время, когда торрент сезонного пакета должен быть на раздаче перед остановкой, при пустом значении используется значение по умолчанию клиента загрузки",
"IndexerSettingsSeasonPackSeedTimeHelpText": "Время, когда торрент полного сезона должен быть на раздаче, перед остановкой, при пустом значении используется значение загрузчика по умолчанию",
"IndexerSettingsSeedRatioHelpText": "Рейтинг, которого должен достичь торрент перед остановкой, пусто — используется значение по умолчанию клиента загрузки. Рейтинг должен быть не менее 1,0 и соответствовать правилам индексаторов",
"IndexerHDBitsSettingsMediumsHelpText": "Если не указано, используются все параметры.",
"IndexerSettingsApiUrlHelpText": "Не меняйте это, если вы не знаете, что делаете. Поскольку ваш ключ API будет отправлен на этот хост.",
@@ -1500,7 +1500,7 @@
"RejectionCount": "Количество отказов",
"Release": "Релиз",
"ReleaseGroup": "Релиз группа",
"ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).`).",
"ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).",
"ReleaseProfileIndexerHelpText": "Укажите, к какому индексатору применяется профиль",
"ReleaseProfileIndexerHelpTextWarning": "Установка определенного индексатора в профиле релиза приведет к тому, что этот профиль будет применяться только к релизам из этого индексатора.",
"ReleaseProfiles": "Профили релизов",
@@ -1681,7 +1681,7 @@
"ImportListsPlexSettingsWatchlistRSSName": "Список наблюдения Plex RSS",
"IndexerSettingsAnimeCategoriesHelpText": "Выпадающий список, оставьте пустым, чтобы отключить аниме",
"IndexerSettingsMultiLanguageRelease": "Несколько языков",
"IndexerSettingsSeasonPackSeedTime": "Время сидирования сезон-пака",
"IndexerSettingsSeasonPackSeedTime": "Время сидирования полного сезона",
"IndexerValidationUnableToConnectResolutionFailure": "Невозможно подключиться к индексатору. Проверьте подключение к серверу индексатора и DNS. {exceptionMessage}.",
"IndexerValidationUnableToConnectHttpError": "Невозможно подключиться к индексатору. Проверьте настройки DNS и убедитесь, что IPv6 работает или отключен. {exceptionMessage}.",
"IndexerTagSeriesHelpText": "Используйте этот индексатор только для сериалов, имеющих хотя бы один соответствующий тег. Оставьте поле пустым, чтобы использовать его для всех.",
@@ -1696,7 +1696,7 @@
"MetadataSettings": "Настройки метаданных",
"NotificationsAppriseSettingsNotificationType": "Тип информирования об уведомлении",
"NotificationsEmailSettingsFromAddress": "С адреса",
"NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование.",
"NotificationsEmailSettingsUseEncryptionHelpText": "Выбрать режим шифрования: предпочитать шифрование, если оно настроено на сервере; всегда использовать шифрование через SSL (только порт 465) или StartTLS (любой другой порт); никогда не использовать шифрование",
"NotificationsCustomScriptSettingsProviderMessage": "При тестировании будет выполняться сценарий с типом события, установленным на {eventTypeTest}. Убедитесь, что ваш сценарий обрабатывает это правильно",
"NotificationsJoinSettingsApiKeyHelpText": "Ключ API из настроек вашей учетной записи присоединения (нажмите кнопку «Присоединиться к API»).",
"NotificationsGotifySettingsServerHelpText": "URL-адрес сервера Gotify, включая http(s):// и порт, если необходимо",
@@ -2016,7 +2016,7 @@
"Search": "Поиск",
"RestartReloadNote": "Примечание: {appName} автоматически перезапустится и перезагрузит интерфейс пользователя во время процесса восстановления.",
"HealthMessagesInfoBox": "Дополнительную информацию о причине появления этих сообщений о проверке работоспособности можно найти, перейдя по ссылке wiki (значок книги) в конце строки или проверить [журналы]({link}). Если у вас возникли трудности с пониманием этих сообщений, вы можете обратиться в нашу службу поддержки по ссылкам ниже.",
"MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github.",
"MaintenanceRelease": "Технический релиз: исправление ошибок и другие улучшения. Подробнее см. в истории коммитов Github",
"Space": "Пробел",
"SslCertPasswordHelpText": "Пароль для файла pfx",
"SpecialEpisode": "Спец. эпизод",
@@ -2155,12 +2155,34 @@
"UpdatePath": "Обновить путь",
"UpdateSeriesPath": "Обновить путь до сериала",
"ReleasePush": "Через API",
"DownloadClientUTorrentProviderMessage": "Мы настоятельно советуем не использовать uTorrent, т.к. он известен как программа-шифровальщик и в целом вредоносное ПО.",
"CloneImportList": "Копировать список импорта",
"EpisodesInSeason": "{episodeCount} эпизодов в сезоне",
"DefaultNameCopiedImportList": "{name} - копировать",
"DownloadClientItemErrorMessage": "{clientName} сообщает об ошибке: {message}",
"AutoTaggingSpecificationNetwork": "Сеть(и)",
"NotificationsAppriseSettingsIncludePoster": "Добавить постер",
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение"
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение",
"EpisodeMonitoring": "Отслеживание эпизода",
"MonitorEpisodes": "Отслеживать эпизоды",
"MonitorEpisodesModalInfo": "Эта настройка влияет только на отслеживание эпизодов или сезонов внутри сериала. Выбор ничего приведёт к остановке отслеживания сериала",
"ImportListsSimklSettingsUserListTypeHold": "Оставить",
"UserRejectedExtensions": "Дополнительные запрещенные расширения файлов",
"UserRejectedExtensionsTextsExamples": "Примеры: '.ext, .xyz' или 'ext,xyz'",
"DownloadClientQbittorrentSettingsAddSeriesTags": "Добавлять теги сериалов",
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Добавлять теги сериалов к новым торрентам, добавляемым в загрузчик (qBittorrent 4.1.0+)",
"UserRejectedExtensionsHelpText": "Список запрещенных расширений файлов, разделенных запятой (так же нужно включить настройку Считать загрузки неуспешными в настройках индексаторов)",
"IndexerSettingsSeasonPackSeedGoal": "Целевая раздача для полных сезонов",
"IndexerSettingsSeasonPackSeedGoalHelpText": "Следует ли использовать другое значение целевой раздачи для полных сезонов",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Использовать стандартные значения",
"IndexerSettingsSeasonPackSeedRatio": "Соотношение раздачи для полных сезонов",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Использовать целевые значения для полных сезонов",
"IndexerSettingsSeasonPackSeedRatioHelpText": "Соотношение, которое полные сезоны должны достигнуть перед остановкой. Пустое значение приведёт к использованию значения загрузчика по умолчанию. Соотношение должно быть не менее 1.0 и соответствовать правилам индексаторов",
"RemoveRootFolderWithSeriesMessageText": "Вы уверены, что хотите удалить корневой каталог '{path}'? Файлов и папки не будут удалены с диска, а сериалы в этом корневом каталоге останутся в {appName}.",
"DownloadClientTriblerSettingsAnonymityLevel": "Уровень анонимности",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "Количество используемых прокси-серверов для скачивания контента. Чтобы выключить, укажите 0. Прокси-серверы уменьшают скорость скачивания/загрузки. Доп. инфо: {url}",
"DownloadClientTriblerSettingsApiKeyHelpText": "[api].key из triblerd.conf",
"DownloadClientTriblerSettingsDirectoryHelpText": "Не обязательный путь для сохранения загрузок. Оставьте пустым, чтобы использовать путь Tribler по умолчанию",
"DownloadClientTriblerSettingsSafeSeeding": "Безопасная раздача",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Если включено, раздача будет вестись только через прокси-серверы.",
"DownloadClientTriblerProviderMessage": "Интеграция с Tribler находится в ранней экспериментальной стадии. Тестировалось на {clientName} версии {clientVersionRange}."
}

View File

@@ -43,5 +43,6 @@
"DownloadStationStatusExtracting": "Packar upp: {progress}%",
"Duplicate": "Dubblett",
"Yesterday": "Igår",
"EditCustomFormat": "Redigera anpassat format"
"EditCustomFormat": "Redigera anpassat format",
"AbsoluteEpisodeNumber": "Fullständigt Avsnitt Nummer"
}

View File

@@ -1424,7 +1424,7 @@
"Reload": "Tekrar yükle",
"RemoveFailedDownloadsHelpText": "Başarısız indirmeleri indirme istemcisi geçmişinden kaldırın",
"RemoveFromBlocklist": "Kara listeden kaldır",
"RemoveRootFolder": "Kök klasörü kaldır",
"RemoveRootFolder": "Kök Klasörü Kaldır",
"RemoveSelected": "Seçilenleri Kaldır",
"RemovingTag": "Etiket kaldırılıyor",
"RenameFiles": "Yeniden Adlandır",
@@ -1780,18 +1780,18 @@
"ImportListsTraktSettingsWatchListSortingHelpText": "Liste Türü İzlenen ise, listeyi sıralamak için sırayı seçin",
"IndexerSearchNoAutomaticHealthCheckMessage": "Otomatik Arama etkinleştirildiğinde hiçbir indeksleyici kullanılamaz, {appName} herhangi bir otomatik arama sonucu sağlamayacaktır",
"IndexerSettingsApiUrlHelpText": "Ne yaptığınızı bilmiyorsanız bunu değiştirmeyin. API anahtarınız ana sunucuya gönderilecektir.",
"IndexerSettingsSeasonPackSeedTimeHelpText": "Bir sezon paketi torrentinin durdurulmadan önce başlatılması gereken süre, boş bırakıldığında indirme istemcisinin varsayılanı kullanılır",
"IndexerSettingsSeasonPackSeedTimeHelpText": "Bir sezon paketi torrentinin durmadan önce paylaşılması gereken süre. Boş bırakılırsa indirme istemcisinin varsayılanı kullanılır",
"IndexerValidationCloudFlareCaptchaRequired": "Site CloudFlare CAPTCHA tarafından korunmaktadır. Geçerli CAPTCHA belirteci gereklidir.",
"IndexerValidationUnableToConnectServerUnavailable": "İndeksleyiciye bağlanılamıyor, indeksleyicinin sunucusu kullanılamıyor. Daha sonra tekrar deneyin. {exceptionMessage}.",
"ImportListsTraktSettingsUserListUsernameHelpText": "İçe aktarılacak Liste için Kullanıcı Adı (Yetkili Kullanıcı için boş bırakın)",
"ImportListsTraktSettingsYearsSeriesHelpText": "Diziyi yıla veya yıl aralığına göre filtreleyin",
"IndexerHDBitsSettingsMediums": "Ortamlar",
"IndexerSearchNoAvailableIndexersHealthCheckMessage": "Son zamanlardaki indeksleyici hataları nedeniyle tüm arama yeteneğine sahip indeksleyiciler geçici olarak kullanılamıyor",
"IndexerSettingsSeasonPackSeedTime": "Sezon Paketi Seed Süresi",
"IndexerSettingsSeasonPackSeedTime": "Sezon Paketi Gönderme (Seed) Süresi",
"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.",
"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.",
"IndexerSettingsMinimumSeeders": "Minimum Seeder",
"IndexerSettingsRssUrl": "RSS URL",
@@ -2152,7 +2152,6 @@
"QualityDefinitionsSizeNotice": "Boyut kısıtlamaları Kalite Profillerine taşındı",
"NotificationsTelegramSettingsLinkPreview": "Bağlantı Önizlemesi",
"NotificationsTelegramSettingsLinkPreviewHelpText": "Telegram bildiriminde hangi bağlantının önizleneceğini belirler. Devre dışı bırakmak için 'Hiçbiri'ni seçin",
"DownloadClientUTorrentProviderMessage": "uTorrent'in kripto para madenciliği, kötü amaçlı yazılım ve reklam içerme geçmişi vardır, bu nedenle farklı bir istemci seçmenizi önemle tavsiye ederiz.",
"NotificationsPushcutSettingsIncludePoster": "Posteri Dahil Et",
"NotificationsPushcutSettingsMetadataLinks": "Meta Veri Bağlantıları",
"NotificationsPushcutSettingsMetadataLinksHelpText": "Bildirim içeriğine meta verilerin bağlantılarını ekleyin",
@@ -2163,5 +2162,27 @@
"EpisodesInSeason": "Sezondaki {episodeCount} bölüm",
"AutoTaggingSpecificationNetwork": "Ağ(lar)",
"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",
"IndexerSettingsSeasonPackSeedGoalUseStandardGoals": "Standart Hedefleri Kullanın",
"IndexerSettingsSeasonPackSeedGoalUseSeasonPackGoals": "Sezon Paketi Hedeflerini Kullan",
"IndexerSettingsSeasonPackSeedRatio": "Sezon Paketi Paylaşım(Seed) Oranı",
"RemoveRootFolderWithSeriesMessageText": "Kök klasörü silmek istediğinizden emin misiniz? {path} klasörü silinecek. Diskten dosya ve klasörler silinmeyecek, ve bu kök klasördeki diziler {appName} içinden kaldırılmayacak.",
"IndexerSettingsSeasonPackSeedGoal": "Sezon Paketleri için Paylaşım(seed) Hedefi",
"IndexerSettingsSeasonPackSeedGoalHelpText": "Sezon paketleri için farklı paylaşım hedefleri kullanıp kullanmamayı seçin",
"IndexerSettingsSeasonPackSeedRatioHelpText": "Bir sezon paketi torrentinin durmadan önce ulaşması gereken oran. Boş bırakılırsa indirme istemcisinin varsayılanı kullanılır. Oran en az 1.0 olmalı ve indeksleyicinin kurallarına uymalıdır",
"DownloadClientTriblerSettingsAnonymityLevel": "Anonimlik seviyesi",
"DownloadClientTriblerSettingsAnonymityLevelHelpText": "İçerik indirirken kullanılacak proxy sayısı. Devre dışı bırakmak için 0 olarak ayarlayın. Proxyler indirme/yükleme hızını azaltır. Bkz. {url}",
"DownloadClientTriblerSettingsApiKeyHelpText": "triblerd.conf dosyasından [api].key",
"DownloadClientTriblerSettingsDirectoryHelpText": "İsteğe bağlı olarak indirmelerin kaydedileceği konum. Varsayılan Tribler konumunu kullanmak için boş bırakın",
"DownloadClientTriblerSettingsSafeSeeding": "Güvenli Paylaşım",
"DownloadClientTriblerSettingsSafeSeedingHelpText": "Etkinleştirildiğinde, yalnızca proxyler üzerinden paylaşım(seed) yapar.",
"DownloadClientTriblerProviderMessage": "Tribler entegrasyonu oldukça deneyseldir. {clientName} istemcisinin {clientVersionRange} sürüm aralığında test edilmiştir."
}

View File

@@ -2123,7 +2123,6 @@
"Airs": "Ефіри",
"DoneEditingSizes": "Редагування розмірів завершено",
"IndexerSettingsFailDownloads": "Не вдалося завантажити",
"DownloadClientUTorrentProviderMessage": "uTorrent має історію включення криптомайнерів, шкідливого програмного забезпечення та реклами. Ми наполегливо рекомендуємо вибрати інший клієнт.",
"EditSelectedCustomFormats": "Змінити вибрані власні формати",
"EditSizes": "Змінити розміри",
"FailedToFetchSettings": "Не вдалося отримати налаштування",

View File

@@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
@@ -31,6 +32,7 @@ namespace NzbDrone.Core.MediaFiles
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly IDetectSample _detectSample;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigService _configService;
private readonly Logger _logger;
public DownloadedEpisodesImportService(IDiskProvider diskProvider,
@@ -41,6 +43,7 @@ namespace NzbDrone.Core.MediaFiles
IImportApprovedEpisodes importApprovedEpisodes,
IDetectSample detectSample,
IRuntimeInfo runtimeInfo,
IConfigService configService,
Logger logger)
{
_diskProvider = diskProvider;
@@ -51,6 +54,7 @@ namespace NzbDrone.Core.MediaFiles
_importApprovedEpisodes = importApprovedEpisodes;
_detectSample = detectSample;
_runtimeInfo = runtimeInfo;
_configService = configService;
_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))
{
_logger.Debug("[{0}] has an unsupported extension: '{1}'", fileInfo.FullName, extension);

View File

@@ -7,6 +7,7 @@ public enum ImportRejectionReason
UnknownSeries,
DangerousFile,
ExecutableFile,
UserRejectedExtension,
ArchiveFile,
SeriesFolder,
InvalidFilePath,

View File

@@ -156,7 +156,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
var downloadClientItem = GetTrackedDownload(downloadId)?.DownloadItem;
var episodes = _episodeService.GetEpisodes(episodeIds);
var finalReleaseGroup = releaseGroup.IsNullOrWhiteSpace()
? Parser.Parser.ParseReleaseGroup(path)
? Parser.ReleaseGroupParser.ParseReleaseGroup(path)
: releaseGroup;
var finalQuality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality;
var finalLanguges =
@@ -218,7 +218,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
SceneSource = SceneSource(series, rootFolder),
ExistingFile = series.Path.IsParentPath(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,
Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality,
IndexerFlags = (IndexerFlags)indexerFlags,
@@ -331,7 +331,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
{
var localEpisode = new LocalEpisode();
localEpisode.Path = file;
localEpisode.ReleaseGroup = Parser.Parser.ParseReleaseGroup(file);
localEpisode.ReleaseGroup = Parser.ReleaseGroupParser.ParseReleaseGroup(file);
localEpisode.Quality = QualityParser.ParseQuality(file);
localEpisode.Languages = LanguageParser.ParseLanguages(file);
localEpisode.Size = _diskProvider.GetFileSize(file);
@@ -483,6 +483,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
var imported = new List<ImportResult>();
var importedTrackedDownload = new List<ManuallyImportedFile>();
var importedUntrackedDownload = new List<ImportResult>();
for (var i = 0; i < message.Files.Count; i++)
{
@@ -545,7 +546,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
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
{
@@ -566,7 +570,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
_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())
{

View File

@@ -14,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (!otherVideoFiles && downloadClientInfo != null && !downloadClientInfo.FullSeason)
{
return Parser.Parser.RemoveFileExtension(downloadClientInfo.ReleaseTitle);
return FileExtensions.RemoveFileExtension(downloadClientInfo.ReleaseTitle);
}
var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath());

View File

@@ -1,11 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
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",
".bz2",
@@ -20,8 +30,7 @@ namespace NzbDrone.Core.MediaFiles
".tgz",
".zip"
};
private static List<string> _dangerousExtensions = new List<string>
public static HashSet<string> DangerousExtensions => new(StringComparer.OrdinalIgnoreCase)
{
".arj",
".lnk",
@@ -31,8 +40,7 @@ namespace NzbDrone.Core.MediaFiles
".vbs",
".zipx"
};
private static List<string> _executableExtensions = new List<string>
public static HashSet<string> ExecutableExtensions => new(StringComparer.OrdinalIgnoreCase)
{
".bat",
".cmd",
@@ -40,8 +48,20 @@ namespace NzbDrone.Core.MediaFiles
".sh"
};
public static HashSet<string> ArchiveExtensions => new HashSet<string>(_archiveExtensions, StringComparer.OrdinalIgnoreCase);
public static HashSet<string> DangerousExtensions => new HashSet<string>(_dangerousExtensions, StringComparer.OrdinalIgnoreCase);
public static HashSet<string> ExecutableExtensions => new HashSet<string>(_executableExtensions, StringComparer.OrdinalIgnoreCase);
public static string RemoveFileExtension(string title)
{
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;
}
}
}

View File

@@ -8,6 +8,7 @@ namespace NzbDrone.Core.MediaFiles
public interface IMediaFileRepository : IBasicRepository<EpisodeFile>
{
List<EpisodeFile> GetFilesBySeries(int seriesId);
List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<EpisodeFile> GetFilesWithoutMediaInfo();
List<EpisodeFile> GetFilesWithRelativePath(int seriesId, string relativePath);
@@ -26,6 +27,11 @@ namespace NzbDrone.Core.MediaFiles
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)
{
return Query(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber).ToList();

View File

@@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
void Update(List<EpisodeFile> episodeFiles);
void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason);
List<EpisodeFile> GetFilesBySeries(int seriesId);
List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds);
List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<EpisodeFile> GetFiles(IEnumerable<int> ids);
List<EpisodeFile> GetFilesWithoutMediaInfo();
@@ -71,6 +72,11 @@ namespace NzbDrone.Core.MediaFiles
return _mediaFileRepository.GetFilesBySeries(seriesId);
}
public List<EpisodeFile> GetFilesBySeriesIds(List<int> seriesIds)
{
return _mediaFileRepository.GetFilesBySeriesIds(seriesIds);
}
public List<EpisodeFile> GetFilesBySeason(int seriesId, int seasonNumber)
{
return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber);

View File

@@ -210,7 +210,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
if (videoFormat == "mpeg4" || videoFormat.Contains("msmpeg4"))
{
if (videoCodecID == "XVID")
if (videoCodecID.ToUpperInvariant() == "XVID")
{
return "XviD";
}
@@ -293,7 +293,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
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)
{

View File

@@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles
{
List<RenameEpisodeFilePreview> GetRenamePreviews(int seriesId);
List<RenameEpisodeFilePreview> GetRenamePreviews(int seriesId, int seasonNumber);
List<RenameEpisodeFilePreview> GetRenamePreviews(List<int> seriesIds);
}
public class RenameEpisodeFileService : IRenameEpisodeFileService,
@@ -75,6 +76,25 @@ namespace NzbDrone.Core.MediaFiles
.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)
{
foreach (var f in files)

View File

@@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.TorrentInfo
{
try
{
return Torrent.Load(fileContents).InfoHash.ToHex();
return Torrent.Load(fileContents).InfoHashes.V1OrV2.ToHex();
}
catch
{

View File

@@ -26,7 +26,6 @@ namespace NzbDrone.Core.MediaFiles
private readonly IConfigService _configService;
private readonly IEpisodeService _episodeService;
private readonly Logger _logger;
private static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public UpdateEpisodeFileService(IDiskProvider diskProvider,
IConfigService configService,
@@ -47,90 +46,48 @@ namespace NzbDrone.Core.MediaFiles
private bool ChangeFileDate(EpisodeFile episodeFile, Series series, List<Episode> episodes)
{
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath);
var airDateUtc = episodes.First().AirDateUtc;
switch (_configService.FileDate)
if (!airDateUtc.HasValue)
{
case FileDateType.LocalAirDate:
{
var airDate = episodes.First().AirDate;
var airTime = series.AirTime;
if (airDate.IsNullOrWhiteSpace() || airTime.IsNullOrWhiteSpace())
{
return false;
}
return ChangeFileDateToLocalAirDate(episodeFilePath, airDate, airTime);
}
case FileDateType.UtcAirDate:
{
var airDateUtc = episodes.First().AirDateUtc;
if (!airDateUtc.HasValue)
{
return false;
}
return ChangeFileDateToUtcAirDate(episodeFilePath, airDateUtc.Value);
}
return false;
}
return false;
return _configService.FileDate switch
{
FileDateType.LocalAirDate =>
ChangeFileDateToLocalDate(episodeFilePath, airDateUtc.Value.ToLocalTime()),
// Intentionally pass UTC as local per user preference
FileDateType.UtcAirDate =>
ChangeFileDateToLocalDate(
episodeFilePath,
DateTime.SpecifyKind(airDateUtc.Value, DateTimeKind.Local)),
_ => false,
};
}
private bool ChangeFileDateToLocalAirDate(string filePath, string fileDate, string fileTime)
private bool ChangeFileDateToLocalDate(string filePath, DateTime localDate)
{
if (DateTime.TryParse(fileDate + ' ' + fileTime, out var airDate))
{
// avoiding false +ve checks and set date skewing by not using UTC (Windows)
var oldLastWrite = _diskProvider.FileGetLastWrite(filePath);
// FileGetLastWrite returns UTC; convert to local to compare
var oldLastWrite = _diskProvider.FileGetLastWrite(filePath).ToLocalTime();
if (OsInfo.IsNotWindows && airDate < EpochTime)
{
_logger.Debug("Setting date of file to 1970-01-01 as actual airdate is before that time and will not be set properly");
airDate = EpochTime;
}
if (!DateTime.Equals(airDate.WithoutTicks(), oldLastWrite.WithoutTicks()))
{
try
{
_diskProvider.FileSetLastWriteTime(filePath, airDate);
_logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDate);
return true;
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to set date of file [" + filePath + "]");
}
}
}
else
{
_logger.Debug("Could not create valid date to change file [{0}]", filePath);
}
return false;
}
private bool ChangeFileDateToUtcAirDate(string filePath, DateTime airDateUtc)
{
var oldLastWrite = _diskProvider.FileGetLastWrite(filePath);
if (OsInfo.IsNotWindows && airDateUtc < EpochTime)
if (OsInfo.IsNotWindows && localDate.ToUniversalTime() < DateTimeExtensions.EpochTime)
{
_logger.Debug("Setting date of file to 1970-01-01 as actual airdate is before that time and will not be set properly");
airDateUtc = EpochTime;
localDate = DateTimeExtensions.EpochTime.ToLocalTime();
}
if (!DateTime.Equals(airDateUtc.WithoutTicks(), oldLastWrite.WithoutTicks()))
if (!DateTime.Equals(localDate.WithoutTicks(), oldLastWrite.WithoutTicks()))
{
try
{
_diskProvider.FileSetLastWriteTime(filePath, airDateUtc.AddMilliseconds(oldLastWrite.Millisecond));
_logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDateUtc);
// Preserve prior mtime subseconds per https://github.com/Sonarr/Sonarr/issues/7228
var mtime = localDate.WithTicksFrom(oldLastWrite);
_diskProvider.FileSetLastWriteTime(filePath, mtime);
_logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, mtime);
return true;
}

View File

@@ -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_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_FinaleTypes", string.Join("|", episodeFile.Episodes.Value.Select(e => e.FinaleType)));
environmentVariables.Add("Sonarr_EpisodeFile_Quality", episodeFile.Quality.Quality.Name);
environmentVariables.Add("Sonarr_EpisodeFile_QualityVersion", episodeFile.Quality.Revision.Version.ToString());
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_EpisodeTitles", string.Join("|", episodes.Select(e => e.Title)));
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_QualityVersions", string.Join("|", episodeFiles.Select(f => f.Quality.Revision.Version)));
environmentVariables.Add("Sonarr_EpisodeFile_ReleaseGroups", string.Join("|", episodeFiles.Select(f => f.ReleaseGroup)));

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
@@ -20,6 +19,16 @@ namespace NzbDrone.Core.Notifications.Emby
_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)
{
var path = "/Notifications/Admin";
@@ -34,21 +43,7 @@ namespace NzbDrone.Core.Notifications.Emby
ImageUrl = "https://raw.github.com/Sonarr/Sonarr/develop/Logo/64.png"
}.ToJson());
try
{
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;
}
}
ProcessRequest(request, settings);
}
public HashSet<string> GetPaths(MediaBrowserSettings settings, Series series)

View File

@@ -62,8 +62,7 @@ namespace NzbDrone.Core.Notifications.Emby
try
{
_logger.Debug("Testing connection to Emby/Jellyfin : {0}", settings.Address);
Notify(settings, "Test from Sonarr", "Success! MediaBrowser has been successfully configured!");
_proxy.TestConnection(settings);
}
catch (HttpException ex)
{
@@ -71,6 +70,11 @@ namespace NzbDrone.Core.Notifications.Emby
{
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)
{
@@ -78,6 +82,25 @@ namespace NzbDrone.Core.Notifications.Emby
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;
}
}

View File

@@ -20,6 +20,7 @@ namespace NzbDrone.Core.Notifications.Webhook
AirDateUtc = episode.AirDateUtc;
SeriesId = episode.SeriesId;
TvdbId = episode.TvdbId;
FinaleType = episode.FinaleType;
}
public int Id { get; set; }
@@ -31,5 +32,6 @@ namespace NzbDrone.Core.Notifications.Webhook
public DateTime? AirDateUtc { get; set; }
public int SeriesId { get; set; }
public int TvdbId { get; set; }
public string FinaleType { get; set; }
}
}

View File

@@ -15,6 +15,8 @@ namespace NzbDrone.Core.Notifications.Webhook
public int TvMazeId { get; set; }
public int TmdbId { 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 int Year { get; set; }
public List<string> Genres { get; set; }
@@ -36,6 +38,8 @@ namespace NzbDrone.Core.Notifications.Webhook
TvMazeId = series.TvMazeId;
TmdbId = series.TmdbId;
ImdbId = series.ImdbId;
MalIds = series.MalIds;
AniListIds = series.AniListIds;
Type = series.SeriesType;
Year = series.Year;
Genres = series.Genres;

View File

@@ -61,7 +61,8 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("uz", "", "uzb", Language.Uzbek),
new IsoLanguage("ms", "", "msa", Language.Malay),
new IsoLanguage("ur", "", "urd", Language.Urdu),
new IsoLanguage("rm", "", "roh", Language.Romansh)
new IsoLanguage("rm", "", "roh", Language.Romansh),
new IsoLanguage("ka", "", "kat", Language.Georgian)
};
private static readonly Dictionary<string, Language> AlternateIsoCodeMappings = new Dictionary<string, Language>

View File

@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Parser
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(@"(?<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)|(?<georgian>\b(?:geo|ka|kat|georgian)\b)|(?<japanese>\(JA\))|(?<original>\b(?:orig|original)\b)",
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))",
@@ -496,6 +496,16 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Romansh);
}
if (match.Groups["georgian"].Success)
{
languages.Add(Language.Georgian);
}
if (match.Groups["japanese"].Success)
{
languages.Add(Language.Japanese);
}
if (match.Groups["original"].Success)
{
languages.Add(Language.Original);

View File

@@ -43,6 +43,11 @@ namespace NzbDrone.Core.Parser.Model
/// <summary>
/// The release is nuked
/// </summary>
Nuked = 128
Nuked = 128,
/// <summary>
/// The release contains subtitles
/// </summary>
Subtitles = 256
}
}

View File

@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Tv;
@@ -17,45 +18,6 @@ namespace NzbDrone.Core.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[]
{
// Anime - Absolute Episode Number + Title + Season+Episode
@@ -124,7 +86,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// 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),
// 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+)))+)",
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
new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?<absoluteepisode>\d{4}(\.\d{1,2})?(?!\d+))",
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 FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$",
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*?",
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*?",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// 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]))(?=[_.-])",
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 ._-]+\]$",
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})[\]\)]?",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -700,7 +632,7 @@ namespace NzbDrone.Core.Parser
if (ReversedTitleRegex.IsMatch(title))
{
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray();
var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension);
title = string.Concat(new string(titleWithoutExtension), title.AsSpan(titleWithoutExtension.Length));
@@ -710,10 +642,9 @@ namespace NzbDrone.Core.Parser
var simpleTitle = title;
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle);
return simpleTitle;
}
@@ -731,7 +662,7 @@ namespace NzbDrone.Core.Parser
if (ReversedTitleRegex.IsMatch(title))
{
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray();
var titleWithoutExtension = FileExtensions.RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension);
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);
}
var releaseTitle = RemoveFileExtension(title);
var releaseTitle = FileExtensions.RemoveFileExtension(title);
releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]");
foreach (var replace in PreSubstitutionRegex)
foreach (var replace in ParserCommon.PreSubstitutionRegex)
{
if (replace.TryReplace(ref releaseTitle))
{
@@ -755,10 +686,9 @@ namespace NzbDrone.Core.Parser
var simpleTitle = SimpleTitleRegex.Replace(releaseTitle);
// TODO: Quick fix stripping [url] - prefixes and postfixes.
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.WebsitePrefixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.WebsitePostfixRegex.Replace(simpleTitle);
simpleTitle = ParserCommon.CleanTorrentSuffixRegex.Replace(simpleTitle);
simpleTitle = CleanQualityBracketsRegex.Replace(simpleTitle, m =>
{
@@ -810,7 +740,7 @@ namespace NzbDrone.Core.Parser
result.Quality = QualityParser.ParseQuality(title);
Logger.Debug("Quality parsed: {0}", result.Quality);
result.ReleaseGroup = ParseReleaseGroup(releaseTitle);
result.ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(releaseTitle);
var subGroup = GetSubGroup(match);
if (!subGroup.IsNullOrWhiteSpace())
@@ -930,80 +860,9 @@ namespace NzbDrone.Core.Parser
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)
{
title = FileExtensionRegex.Replace(title, m =>
{
var extension = m.Value.ToLower();
if (MediaFiles.MediaFileExtensions.Extensions.Contains(extension) || new[] { ".par2", ".nzb" }.Contains(extension))
{
return string.Empty;
}
return m.Value;
});
return title;
return FileExtensions.RemoveFileExtension(title);
}
public static bool HasMultipleLanguages(string title)
@@ -1309,7 +1168,7 @@ namespace NzbDrone.Core.Parser
return false;
}
var titleWithoutExtension = RemoveFileExtension(title);
var titleWithoutExtension = FileExtensions.RemoveFileExtension(title);
if (RejectHashedReleasesRegexes.Any(v => v.IsMatch(titleWithoutExtension)))
{

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);
}

View File

@@ -353,7 +353,7 @@ namespace NzbDrone.Core.Parser
EpisodeNumbers = new int[1] { episode.EpisodeNumber },
FullSeason = false,
Quality = QualityParser.ParseQuality(releaseTitle),
ReleaseGroup = Parser.ParseReleaseGroup(releaseTitle),
ReleaseGroup = ReleaseGroupParser.ParseReleaseGroup(releaseTitle),
Languages = LanguageParser.ParseLanguages(releaseTitle),
Special = true
};

View File

@@ -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;
}
}

View File

@@ -4,6 +4,7 @@ using System.Linq;
using NzbDrone.Common.Crypto;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
@@ -65,7 +66,7 @@ namespace NzbDrone.Core.Queue
Episode = episode,
Languages = trackedDownload.RemoteEpisode?.Languages ?? new List<Language> { Language.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,
SizeLeft = trackedDownload.DownloadItem.RemainingSize,
TimeLeft = trackedDownload.DownloadItem.RemainingTime,

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