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

Compare commits

...

15 Commits

Author SHA1 Message Date
Elias Benbourenane
ca0bb14027 Allow GetFileSize to follow symlinks 2024-11-14 19:27:56 -08:00
Mark McDowall
3e99917e9d Fixed: Closing on click outside select input and styling on Library Import 2024-11-14 19:27:31 -08:00
Mark McDowall
936cf699ff Improve LanguageSelectInput 2024-11-14 19:27:31 -08:00
Mark McDowall
202190d032 New: Replace 'Ben the Man' release group parsing with 'Ben the Men'
Closes #7365
2024-11-14 19:02:09 -08:00
Mark McDowall
f739fd0900 Fixed: Allow files to be moved from Torrent Blackhole even when remove is disabled 2024-11-14 19:01:38 -08:00
Mark McDowall
88f4016fe0 New: Parse original from release name when specified
Closes #5805
2024-11-14 19:01:17 -08:00
Gauthier
78fb20282d New: Add headers setting in webhook connection 2024-11-14 19:01:05 -08:00
Mark McDowall
6677fd1116 New: Improve stored UI settings for multiple instances under the same host
Closes #7368
2024-11-14 19:00:21 -08:00
Mark McDowall
e28b7c3df6 Fixed: .plexmatch episodes on separate lines
Closes #7362
2024-11-14 19:00:10 -08:00
Bogdan
67a1ecb0fe Console warnings for missing translations on development builds 2024-11-14 18:59:53 -08:00
Mark McDowall
5bc943583c Don't try to process items that didn't import in manual import 2024-11-14 18:59:43 -08:00
Mark McDowall
ceeec091f8 Fixed: Normalize unicode characters when comparing paths for equality
Closes #6657
2024-11-14 18:59:43 -08:00
Bogdan
675e3cd38a New: Labels support for Transmission 4.0
Closes #7300
2024-11-14 18:59:25 -08:00
Weblate
45a62a2e59 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@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/es/
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/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-11-14 18:58:37 -08:00
Sonarr
ae7c07e02f Automated API Docs update
ignore-downstream
2024-11-07 22:42:05 -08:00
51 changed files with 1128 additions and 494 deletions

View File

@@ -1,18 +1,10 @@
.inputContainer {
margin-right: 20px;
min-width: 150px;
div {
margin-top: 10px;
&:first-child {
margin-top: 0;
}
}
}
.label {
margin-bottom: 3px;
margin-bottom: 10px;
font-weight: bold;
}

View File

@@ -56,7 +56,7 @@ function Legend(props) {
if (showCutoffUnmetIcon) {
iconsToShow.push(
<LegendIconItem
name={translate('Cutoff Not Met')}
name={translate('CutoffNotMet')}
icon={icons.EPISODE_FILE}
kind={kinds.WARNING}
fullColorEvents={fullColorEvents}

View File

@@ -10,6 +10,7 @@ import CaptchaInput from './CaptchaInput';
import CheckInput from './CheckInput';
import { FormInputButtonProps } from './FormInputButton';
import FormInputHelpText from './FormInputHelpText';
import KeyValueListInput from './KeyValueListInput';
import NumberInput from './NumberInput';
import OAuthInput from './OAuthInput';
import PasswordInput from './PasswordInput';
@@ -18,6 +19,7 @@ import DownloadClientSelectInput from './Select/DownloadClientSelectInput';
import EnhancedSelectInput from './Select/EnhancedSelectInput';
import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput';
import IndexerSelectInput from './Select/IndexerSelectInput';
import LanguageSelectInput from './Select/LanguageSelectInput';
import MonitorEpisodesSelectInput from './Select/MonitorEpisodesSelectInput';
import MonitorNewItemsSelectInput from './Select/MonitorNewItemsSelectInput';
import ProviderDataSelectInput from './Select/ProviderOptionSelectInput';
@@ -47,6 +49,12 @@ function getComponent(type: InputType) {
case inputTypes.DEVICE:
return DeviceInput;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;
case inputTypes.LANGUAGE_SELECT:
return LanguageSelectInput;
case inputTypes.MONITOR_EPISODES_SELECT:
return MonitorEpisodesSelectInput;

View File

@@ -0,0 +1,21 @@
.inputContainer {
composes: input from '~Components/Form/Input.css';
position: relative;
min-height: 35px;
height: auto;
&.isFocused {
outline: 0;
border-color: var(--inputFocusBorderColor);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor);
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}

View File

@@ -0,0 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'hasError': string;
'hasWarning': string;
'inputContainer': string;
'isFocused': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,104 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { InputOnChange } from 'typings/inputs';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
interface KeyValue {
key: string;
value: string;
}
export interface KeyValueListInputProps {
className?: string;
name: string;
value: KeyValue[];
hasError?: boolean;
hasWarning?: boolean;
keyPlaceholder?: string;
valuePlaceholder?: string;
onChange: InputOnChange<KeyValue[]>;
}
function KeyValueListInput({
className = styles.inputContainer,
name,
value = [],
hasError = false,
hasWarning = false,
keyPlaceholder,
valuePlaceholder,
onChange,
}: KeyValueListInputProps): JSX.Element {
const [isFocused, setIsFocused] = useState(false);
const handleItemChange = useCallback(
(index: number | null, itemValue: KeyValue) => {
const newValue = [...value];
if (index === null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const handleRemoveItem = useCallback(
(index: number) => {
const newValue = [...value];
newValue.splice(index, 1);
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const onFocus = useCallback(() => setIsFocused(true), []);
const onBlur = useCallback(() => {
setIsFocused(false);
const newValue = value.reduce((acc: KeyValue[], v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({ name, value: newValue });
}
}, [value, name, onChange]);
return (
<div
className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{[...value, { key: '', value: '' }].map((v, index) => (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={handleItemChange}
onRemove={handleRemoveItem}
onFocus={onFocus}
onBlur={onBlur}
/>
))}
</div>
);
}
export default KeyValueListInput;

View File

@@ -0,0 +1,35 @@
.itemContainer {
display: flex;
margin-bottom: 3px;
border-bottom: 1px solid var(--inputBorderColor);
&:last-child {
margin-bottom: 0;
border-bottom: 0;
}
}
.keyInputWrapper {
flex: 1 0 0;
}
.valueInputWrapper {
flex: 1 0 0;
min-width: 40px;
}
.buttonWrapper {
flex: 0 0 22px;
}
.keyInput,
.valueInput {
width: 100%;
border: none;
background-color: transparent;
color: var(--textColor);
&::placeholder {
color: var(--helpTextColor);
}
}

View File

@@ -0,0 +1,12 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'buttonWrapper': string;
'itemContainer': string;
'keyInput': string;
'keyInputWrapper': string;
'valueInput': string;
'valueInputWrapper': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,89 @@
import React, { useCallback } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
interface KeyValueListInputItemProps {
index: number;
keyValue: string;
value: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
isNew: boolean;
onChange: (index: number, itemValue: { key: string; value: string }) => void;
onRemove: (index: number) => void;
onFocus: () => void;
onBlur: () => void;
}
function KeyValueListInputItem({
index,
keyValue,
value,
keyPlaceholder = 'Key',
valuePlaceholder = 'Value',
isNew,
onChange,
onRemove,
onFocus,
onBlur,
}: KeyValueListInputItemProps): JSX.Element {
const handleKeyChange = useCallback(
({ value: keyValue }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, value, onChange]
);
const handleValueChange = useCallback(
({ value }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, keyValue, onChange]
);
const handleRemovePress = useCallback(() => {
onRemove(index);
}, [index, onRemove]);
return (
<div className={styles.itemContainer}>
<div className={styles.keyInputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={handleKeyChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.valueInputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={handleValueChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{isNew ? null : (
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={handleRemovePress}
/>
)}
</div>
</div>
);
}
export default KeyValueListInputItem;

View File

@@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.CHECK;
case 'device':
return inputTypes.DEVICE;
case 'keyValueList':
return inputTypes.KEY_VALUE_LIST;
case 'password':
return inputTypes.PASSWORD;
case 'number':

View File

@@ -192,7 +192,7 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
const { top, bottom } = data.offsets.reference;
const windowHeight = window.innerHeight;
if (/^botton/.test(data.placement)) {
if (/^bottom/.test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
@@ -233,18 +233,12 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
}, [handleWindowClick]);
const handlePress = useCallback(() => {
if (isOpen) {
removeListener();
} else {
addListener();
}
if (!isOpen && onOpen) {
onOpen();
}
setIsOpen(!isOpen);
}, [isOpen, setIsOpen, addListener, removeListener, onOpen]);
}, [isOpen, setIsOpen, onOpen]);
const handleSelect = useCallback(
(newValue: ArrayElement<V>) => {
@@ -411,6 +405,16 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
}
});
useEffect(() => {
if (isOpen) {
addListener();
} else {
removeListener();
}
return removeListener;
}, [isOpen, addListener, removeListener]);
return (
<div>
<Manager>

View File

@@ -1,43 +1,95 @@
import React, { useMemo } from 'react';
import { EnhancedSelectInputChanged } from 'typings/inputs';
import React, { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import Language from 'Language/Language';
import createFilteredLanguagesSelector from 'Store/Selectors/createFilteredLanguagesSelector';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput, {
EnhancedSelectInputValue,
} from './EnhancedSelectInput';
interface LanguageSelectInputProps {
interface LanguageSelectInputOnChangeProps {
name: string;
value: number;
values: EnhancedSelectInputValue<number>[];
onChange: (change: EnhancedSelectInputChanged<number>) => void;
value: number | string | Language;
}
function LanguageSelectInput({
values,
interface LanguageSelectInputProps {
name: string;
value: number | string | Language;
includeNoChange: boolean;
includeNoChangeDisabled?: boolean;
includeMixed: boolean;
onChange: (payload: LanguageSelectInputOnChangeProps) => void;
}
export default function LanguageSelectInput({
value,
includeNoChange,
includeNoChangeDisabled,
includeMixed,
onChange,
...otherProps
}: LanguageSelectInputProps) {
const mappedValues = useMemo(() => {
const minId = values.reduce(
(min: number, v) => (v.key < 1 ? v.key : min),
values[0].key
const { items } = useSelector(createFilteredLanguagesSelector(true));
const values = useMemo(() => {
const result: EnhancedSelectInputValue<number | string>[] = items.map(
(item) => {
return {
key: item.id,
value: item.name,
};
}
);
return values.map(({ key, value }) => {
return {
key,
value,
dividerAfter: minId < 1 ? key === minId : false,
};
});
}, [values]);
if (includeNoChange) {
result.unshift({
key: 'noChange',
value: translate('NoChange'),
isDisabled: includeNoChangeDisabled,
});
}
if (includeMixed) {
result.unshift({
key: 'mixed',
value: `(${translate('Mixed')})`,
isDisabled: true,
});
}
return result;
}, [includeNoChange, includeNoChangeDisabled, includeMixed, items]);
const selectValue =
typeof value === 'number' || typeof value === 'string' ? value : value.id;
const handleChange = useCallback(
(payload: LanguageSelectInputOnChangeProps) => {
if (typeof value === 'number') {
onChange(payload);
} else {
const language = items.find((i) => i.id === payload.value);
onChange({
...payload,
value: language
? {
id: language.id,
name: language.name,
}
: ({ id: payload.value } as Language),
});
}
},
[value, items, onChange]
);
return (
<EnhancedSelectInput
{...otherProps}
values={mappedValues}
onChange={onChange}
value={selectValue}
values={values}
onChange={handleChange}
/>
);
}
export default LanguageSelectInput;

View File

@@ -2,6 +2,7 @@ export const AUTO_COMPLETE = 'autoComplete';
export const CAPTCHA = 'captcha';
export const CHECK = 'check';
export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
@@ -31,6 +32,7 @@ export const all = [
CAPTCHA,
CHECK,
DEVICE,
KEY_VALUE_LIST,
MONITOR_EPISODES_SELECT,
MONITOR_NEW_ITEMS_SELECT,
FLOAT,

View File

@@ -66,10 +66,10 @@ class UISettings extends Component {
isFetching,
error,
settings,
languages,
hasSettings,
onInputChange,
onSavePress,
languages,
...otherProps
} = this.props;
@@ -213,9 +213,8 @@ class UISettings extends Component {
<FormGroup>
<FormLabel>{translate('UiLanguage')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
type={inputTypes.LANGUAGE_SELECT}
name="uiLanguage"
values={languages}
helpText={translate('UiLanguageHelpText')}
helpTextWarning={translate('BrowserReloadRequired')}
onChange={onInputChange}
@@ -244,8 +243,8 @@ UISettings.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
hasSettings: PropTypes.bool.isRequired,
onSavePress: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired
};

View File

@@ -96,14 +96,22 @@ function merge(initialState, persistedState) {
return computedState;
}
const KEY = 'sonarr';
const config = {
slicer,
serialize,
merge,
key: 'sonarr'
key: window.Sonarr.instanceName.toLowerCase().replace(/ /g, '_') || KEY
};
export default function createPersistState() {
// Migrate existing local storage value to new key if it does not already exist.
// Leave old value as-is in case there are multiple instances using the same key.
if (config.key !== KEY && localStorage.getItem(KEY) && !localStorage.getItem(config.key)) {
localStorage.setItem(config.key, localStorage.getItem(KEY));
}
// Migrate existing local storage before proceeding
const persistedState = JSON.parse(localStorage.getItem(config.key));
migrate(persistedState);

View File

@@ -0,0 +1,28 @@
import { createSelector } from 'reselect';
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
import Language from 'Language/Language';
import createLanguagesSelector from './createLanguagesSelector';
export default function createFilteredLanguagesSelector(filterUnknown = false) {
const filterItems = ['Any', 'Original'];
if (filterUnknown) {
filterItems.push('Unknown');
}
return createSelector(createLanguagesSelector(), (languages) => {
const { isFetching, isPopulated, error, items } =
languages as LanguageSettingsAppState;
const filteredLanguages = items.filter(
(lang: Language) => !filterItems.includes(lang.name)
);
return {
isFetching,
isPopulated,
error,
items: filteredLanguages,
};
});
}

View File

@@ -27,6 +27,12 @@ export default function translate(
key: string,
tokens: Record<string, string | number | boolean> = {}
) {
const { isProduction = true } = window.Sonarr;
if (!isProduction && !(key in translations)) {
console.warn(`Missing translation for key: ${key}`);
}
const translation = translations[key] || key;
tokens.appName = 'Sonarr';

View File

@@ -7,5 +7,6 @@ interface Window {
theme: string;
urlBase: string;
version: string;
isProduction: boolean;
};
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -434,5 +435,14 @@ namespace NzbDrone.Common.Test
{
parentPath.GetRelativePath(childPath).Should().Be(relativePath);
}
[Test]
public void should_be_equal_with_different_unicode_representations()
{
var path1 = @"C:\Test\file.mkv".AsOsAgnostic().Normalize(NormalizationForm.FormC);
var path2 = @"C:\Test\file.mkv".AsOsAgnostic().Normalize(NormalizationForm.FormD);
path1.PathEquals(path2);
}
}
}

View File

@@ -189,6 +189,18 @@ namespace NzbDrone.Common.Disk
}
var fi = new FileInfo(path);
// If the file is a symlink, resolve the target path and get the size of the target file.
if (fi.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
var targetPath = fi.ResolveLinkTarget(true)?.FullName;
if (targetPath != null)
{
fi = new FileInfo(targetPath);
}
}
return fi.Length;
}

View File

@@ -148,10 +148,5 @@ namespace NzbDrone.Common.Extensions
{
return string.Join(separator, source.Select(predicate));
}
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer = null)
{
return new HashSet<T>(source, comparer);
}
}
}

View File

@@ -54,6 +54,10 @@ namespace NzbDrone.Common.Extensions
public static bool PathEquals(this string firstPath, string secondPath, StringComparison? comparison = null)
{
// Normalize paths to ensure unicode characters are represented the same way
firstPath = firstPath.Normalize();
secondPath = secondPath?.Normalize();
if (!comparison.HasValue)
{
comparison = DiskProviderBase.PathStringComparison;

View File

@@ -21,10 +21,10 @@ namespace NzbDrone.Common
{
if (OsInfo.IsWindows)
{
return obj.CleanFilePath().ToLower().GetHashCode();
return obj.CleanFilePath().Normalize().ToLower().GetHashCode();
}
return obj.CleanFilePath().GetHashCode();
return obj.CleanFilePath().Normalize().GetHashCode();
}
}
}

View File

@@ -34,7 +34,8 @@ namespace NzbDrone.Common.Reflection
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Version)
|| type == typeof(decimal);
|| type == typeof(decimal)
|| (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>));
}
public static bool IsReadable(this PropertyInfo propertyInfo)

View File

@@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole
VerifyCompleted(result);
result.CanBeRemoved.Should().BeFalse();
result.CanBeRemoved.Should().BeTrue();
result.CanMoveFiles.Should().BeFalse();
}

View File

@@ -13,6 +13,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
[TestFixture]
public class TransmissionFixture : TransmissionFixtureBase<Transmission>
{
[SetUp]
public void Setup_Transmission()
{
Mocker.GetMock<ITransmissionProxy>()
.Setup(v => v.GetClientVersion(It.IsAny<TransmissionSettings>(), It.IsAny<bool>()))
.Returns("4.0.6");
}
[Test]
public void queued_item_should_have_required_properties()
{
@@ -272,7 +280,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
public void should_only_check_version_number(string version)
{
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetClientVersion(It.IsAny<TransmissionSettings>()))
.Setup(s => s.GetClientVersion(It.IsAny<TransmissionSettings>(), true))
.Returns(version);
Subject.Test().IsValid.Should().BeTrue();

View File

@@ -29,7 +29,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass"
Password = "pass",
TvCategory = ""
};
Subject.Definition = new DownloadClientDefinition();
@@ -152,7 +153,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests
}
Mocker.GetMock<ITransmissionProxy>()
.Setup(s => s.GetTorrents(It.IsAny<TransmissionSettings>()))
.Setup(s => s.GetTorrents(null, It.IsAny<TransmissionSettings>()))
.Returns(torrents);
}

View File

@@ -432,6 +432,16 @@ namespace NzbDrone.Core.Test.ParserTests
result.Languages.Should().Contain(Language.English);
}
[TestCase("Series.Title.S01E01.Original.1080P.WEB.H264-RlsGrp")]
[TestCase("Series.Title.S01E01.Orig.1080P.WEB.H264-RlsGrp")]
[TestCase("Series / S1E1-10 of 10 [2023, HEVC, HDR10, Dolby Vision, WEB-DL 2160p] [Hybrid] 3 XX + Original")]
public void should_parse_original_title_from_release_name(string postTitle)
{
var result = Parser.Parser.ParseTitle(postTitle);
result.Languages.Count.Should().Be(1);
result.Languages.Should().Contain(Language.Original);
}
[TestCase("Остання серія (Сезон 1) / The Last Series (Season 1) (2024) WEB-DLRip-AVC 2xUkr/Eng | Sub Ukr/Eng")]
[TestCase("Справжня серія (Сезон 1-3) / True Series (Season 1-3) (2014-2019) BDRip-AVC 3xUkr/Eng | Ukr/Eng")]
[TestCase("Серія (Сезон 1-3) / The Series (Seasons 1-3) (2019-2022) BDRip-AVC 4xUkr/Eng | Sub 2xUkr/Eng")]

View File

@@ -89,7 +89,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title S01 1080p Blu-ray Remux AVC DTS-HD MA 2.0 - BluDragon", "BluDragon")]
[TestCase("Example (2013) S01E01 (1080p iP WEBRip x265 SDR AAC 2.0 English - DarQ)", "DarQ")]
[TestCase("Series.Title.S08E03.720p.WEB.DL.AAC2.0.H.264.KCRT", "KCRT")]
[TestCase("S02E05 2160p WEB-DL DV HDR ENG DDP5.1 Atmos H265 MP4-BEN THE MAN", "BEN THE MAN")]
[TestCase("Series Title S02E05 2160p WEB-DL DV HDR ENG DDP5.1 Atmos H265 MP4-BEN THE MEN", "BEN THE MEN")]
[TestCase("Series Title S02E05 2160p AMZN WEB-DL DV HDR10 PLUS DDP5 1 Atmos H265 MKV-BEN THE MEN-xpost", "BEN THE MEN")]
[TestCase("Series.S01E05.1080p.WEB-DL.DDP5.1.H264-BEN.THE.MEN", "BEN.THE.MEN")]
public void should_parse_exception_release_group(string title, string expected)
{
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);

View File

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

View File

@@ -104,9 +104,8 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
Status = item.Status
};
queueItem.CanMoveFiles = queueItem.CanBeRemoved =
queueItem.DownloadClientInfo.RemoveCompletedDownloads &&
!Settings.ReadOnly;
queueItem.CanMoveFiles = !Settings.ReadOnly;
queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads;
yield return queueItem;
}

View File

@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
foreach (var item in _scanWatchFolder.GetItems(Settings.WatchFolder, ScanGracePeriod))
{
yield return new DownloadClientItem
var queueItem = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = Definition.Name + "_" + item.DownloadId,
@@ -72,10 +72,12 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
OutputPath = item.OutputPath,
Status = item.Status,
CanBeRemoved = true,
CanMoveFiles = true
};
queueItem.CanMoveFiles = true;
queueItem.CanBeRemoved = queueItem.DownloadClientInfo.RemoveCompletedDownloads;
yield return queueItem;
}
}

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
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;
@@ -15,6 +17,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public class Transmission : TransmissionBase
{
public override string Name => "Transmission";
public override bool SupportsLabels => HasClientVersion(4, 0);
public Transmission(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
@@ -28,9 +33,48 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
}
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
{
if (!SupportsLabels)
{
throw new NotSupportedException($"{Name} does not support marking items as imported");
}
// set post-import category
if (Settings.TvImportedCategory.IsNotNullOrWhiteSpace() &&
Settings.TvImportedCategory != Settings.TvCategory)
{
var hash = downloadClientItem.DownloadId.ToLowerInvariant();
var torrent = _proxy.GetTorrents(new[] { hash }, Settings).FirstOrDefault();
if (torrent == null)
{
_logger.Warn("Could not find torrent with hash \"{0}\" in Transmission.", hash);
return;
}
try
{
var labels = torrent.Labels.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
labels.Add(Settings.TvImportedCategory);
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
labels.Remove(Settings.TvCategory);
}
_proxy.SetTorrentLabels(hash, labels, Settings);
}
catch (DownloadClientException ex)
{
_logger.Warn(ex, "Failed to set post-import torrent label \"{0}\" for {1} in Transmission.", Settings.TvImportedCategory, downloadClientItem.Title);
}
}
}
protected override ValidationFailure ValidateVersion()
{
var versionString = _proxy.GetClientVersion(Settings);
var versionString = _proxy.GetClientVersion(Settings, true);
_logger.Debug("Transmission version information: {0}", versionString);
@@ -44,7 +88,5 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return null;
}
public override string Name => "Transmission";
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Disk;
@@ -18,6 +19,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public abstract class TransmissionBase : TorrentClientBase<TransmissionSettings>
{
public abstract bool SupportsLabels { get; }
protected readonly ITransmissionProxy _proxy;
public TransmissionBase(ITransmissionProxy proxy,
@@ -37,7 +40,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public override IEnumerable<DownloadClientItem> GetItems()
{
var configFunc = new Lazy<TransmissionConfig>(() => _proxy.GetConfig(Settings));
var torrents = _proxy.GetTorrents(Settings);
var torrents = _proxy.GetTorrents(null, Settings);
var items = new List<DownloadClientItem>();
@@ -45,36 +48,45 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
var outputPath = new OsPath(torrent.DownloadDir);
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
if (Settings.TvCategory.IsNotNullOrWhiteSpace() && SupportsLabels && torrent.Labels is { Count: > 0 })
{
if (!new OsPath(Settings.TvDirectory).Contains(outputPath))
if (!torrent.Labels.Contains(Settings.TvCategory, StringComparer.InvariantCultureIgnoreCase))
{
continue;
}
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
else
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.TvCategory))
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
{
continue;
if (!new OsPath(Settings.TvDirectory).Contains(outputPath))
{
continue;
}
}
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
var directories = outputPath.FullPath.Split('\\', '/');
if (!directories.Contains(Settings.TvCategory))
{
continue;
}
}
}
outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath);
var item = new DownloadClientItem();
item.DownloadId = torrent.HashString.ToUpper();
item.Category = Settings.TvCategory;
item.Title = torrent.Name;
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
item.OutputPath = GetOutputPath(outputPath, torrent);
item.TotalSize = torrent.TotalSize;
item.RemainingSize = torrent.LeftUntilDone;
item.SeedRatio = torrent.DownloadedEver <= 0 ? 0 :
(double)torrent.UploadedEver / torrent.DownloadedEver;
var item = new DownloadClientItem
{
DownloadId = torrent.HashString.ToUpper(),
Category = Settings.TvCategory,
Title = torrent.Name,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace() && SupportsLabels),
OutputPath = GetOutputPath(outputPath, torrent),
TotalSize = torrent.TotalSize,
RemainingSize = torrent.LeftUntilDone,
SeedRatio = torrent.DownloadedEver <= 0 ? 0 : (double)torrent.UploadedEver / torrent.DownloadedEver
};
if (torrent.Eta >= 0)
{
@@ -300,7 +312,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
try
{
_proxy.GetTorrents(Settings);
_proxy.GetTorrents(null, Settings);
}
catch (Exception ex)
{
@@ -310,5 +322,15 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return null;
}
protected bool HasClientVersion(int major, int minor)
{
var rawVersion = _proxy.GetClientVersion(Settings);
var versionResult = Regex.Match(rawVersion, @"(?<!\(|(\d|\.)+)(\d|\.)+(?!\)|(\d|\.)+)").Value;
var clientVersion = Version.Parse(versionResult);
return clientVersion >= new Version(major, minor);
}
}
}

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using Newtonsoft.Json.Linq;
using NLog;
@@ -12,15 +15,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
public interface ITransmissionProxy
{
List<TransmissionTorrent> GetTorrents(TransmissionSettings settings);
IReadOnlyCollection<TransmissionTorrent> GetTorrents(IReadOnlyCollection<string> hashStrings, TransmissionSettings settings);
void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings);
void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings);
void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings);
TransmissionConfig GetConfig(TransmissionSettings settings);
string GetProtocolVersion(TransmissionSettings settings);
string GetClientVersion(TransmissionSettings settings);
string GetClientVersion(TransmissionSettings settings, bool force = false);
void RemoveTorrent(string hash, bool removeData, TransmissionSettings settings);
void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings);
void SetTorrentLabels(string hash, IEnumerable<string> labels, TransmissionSettings settings);
}
public class TransmissionProxy : ITransmissionProxy
@@ -28,50 +32,66 @@ namespace NzbDrone.Core.Download.Clients.Transmission
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
private ICached<string> _authSessionIDCache;
private readonly ICached<string> _authSessionIdCache;
private readonly ICached<string> _versionCache;
public TransmissionProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
_authSessionIDCache = cacheManager.GetCache<string>(GetType(), "authSessionID");
_authSessionIdCache = cacheManager.GetCache<string>(GetType(), "authSessionID");
_versionCache = cacheManager.GetCache<string>(GetType(), "versions");
}
public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings)
public IReadOnlyCollection<TransmissionTorrent> GetTorrents(IReadOnlyCollection<string> hashStrings, TransmissionSettings settings)
{
var result = GetTorrentStatus(settings);
var result = GetTorrentStatus(hashStrings, settings);
var torrents = ((JArray)result.Arguments["torrents"]).ToObject<List<TransmissionTorrent>>();
var torrents = ((JArray)result.Arguments["torrents"]).ToObject<ReadOnlyCollection<TransmissionTorrent>>();
return torrents;
}
public void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("filename", torrentUrl);
arguments.Add("paused", settings.AddPaused);
var arguments = new Dictionary<string, object>
{
{ "filename", torrentUrl },
{ "paused", settings.AddPaused }
};
if (!downloadDirectory.IsNullOrWhiteSpace())
{
arguments.Add("download-dir", downloadDirectory);
}
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
arguments.Add("labels", new List<string> { settings.TvCategory });
}
ProcessRequest("torrent-add", arguments, settings);
}
public void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("metainfo", Convert.ToBase64String(torrentData));
arguments.Add("paused", settings.AddPaused);
var arguments = new Dictionary<string, object>
{
{ "metainfo", Convert.ToBase64String(torrentData) },
{ "paused", settings.AddPaused }
};
if (!downloadDirectory.IsNullOrWhiteSpace())
{
arguments.Add("download-dir", downloadDirectory);
}
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
arguments.Add("labels", new List<string> { settings.TvCategory });
}
ProcessRequest("torrent-add", arguments, settings);
}
@@ -82,8 +102,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return;
}
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new[] { hash });
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hash } }
};
if (seedConfiguration.Ratio != null)
{
@@ -97,6 +119,12 @@ namespace NzbDrone.Core.Download.Clients.Transmission
arguments.Add("seedIdleMode", 1);
}
// Avoid extraneous request if no limits are to be set
if (arguments.All(arg => arg.Key == "ids"))
{
return;
}
ProcessRequest("torrent-set", arguments, settings);
}
@@ -107,11 +135,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return config.RpcVersion;
}
public string GetClientVersion(TransmissionSettings settings)
public string GetClientVersion(TransmissionSettings settings, bool force = false)
{
var config = GetConfig(settings);
var cacheKey = $"version:{$"{GetBaseUrl(settings)}:{settings.Password}".SHA256Hash()}";
return config.Version;
if (force)
{
_versionCache.Remove(cacheKey);
}
return _versionCache.Get(cacheKey, () => GetConfig(settings).Version, TimeSpan.FromHours(6));
}
public TransmissionConfig GetConfig(TransmissionSettings settings)
@@ -124,21 +157,36 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public void RemoveTorrent(string hashString, bool removeData, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new string[] { hashString });
arguments.Add("delete-local-data", removeData);
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hashString } },
{ "delete-local-data", removeData }
};
ProcessRequest("torrent-remove", arguments, settings);
}
public void MoveTorrentToTopInQueue(string hashString, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>();
arguments.Add("ids", new string[] { hashString });
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hashString } }
};
ProcessRequest("queue-move-top", arguments, settings);
}
public void SetTorrentLabels(string hash, IEnumerable<string> labels, TransmissionSettings settings)
{
var arguments = new Dictionary<string, object>
{
{ "ids", new List<string> { hash } },
{ "labels", labels.ToImmutableHashSet() }
};
ProcessRequest("torrent-set", arguments, settings);
}
private TransmissionResponse GetSessionVariables(TransmissionSettings settings)
{
// Retrieve transmission information such as the default download directory, bandwidth throttling and seed ratio.
@@ -151,14 +199,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return ProcessRequest("session-stats", null, settings);
}
private TransmissionResponse GetTorrentStatus(TransmissionSettings settings)
{
return GetTorrentStatus(null, settings);
}
private TransmissionResponse GetTorrentStatus(IEnumerable<string> hashStrings, TransmissionSettings settings)
{
var fields = new string[]
var fields = new List<string>
{
"id",
"hashString", // Unique torrent ID. Use this instead of the client id?
@@ -179,11 +222,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission
"seedIdleLimit",
"seedIdleMode",
"fileCount",
"file-count"
"file-count",
"labels"
};
var arguments = new Dictionary<string, object>();
arguments.Add("fields", fields);
var arguments = new Dictionary<string, object>
{
{ "fields", fields }
};
if (hashStrings != null)
{
@@ -195,9 +241,14 @@ namespace NzbDrone.Core.Download.Clients.Transmission
return result;
}
private string GetBaseUrl(TransmissionSettings settings)
{
return HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
}
private HttpRequestBuilder BuildRequest(TransmissionSettings settings)
{
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
var requestBuilder = new HttpRequestBuilder(GetBaseUrl(settings))
.Resource("rpc")
.Accept(HttpAccept.Json);
@@ -212,11 +263,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
{
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
var sessionId = _authSessionIDCache.Find(authKey);
var sessionId = _authSessionIdCache.Find(authKey);
if (sessionId == null || reauthenticate)
{
_authSessionIDCache.Remove(authKey);
_authSessionIdCache.Remove(authKey);
var authLoginRequest = BuildRequest(settings).Build();
authLoginRequest.SuppressHttpError = true;
@@ -244,7 +295,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
_logger.Debug("Transmission authentication succeeded.");
_authSessionIDCache.Set(authKey, sessionId);
_authSessionIdCache.Set(authKey, sessionId);
}
requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId);

View File

@@ -32,6 +32,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
Host = "localhost";
Port = 9091;
UrlBase = "/transmission/";
TvCategory = "tv-sonarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@@ -59,16 +60,19 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategorySubFolderHelpText")]
public string TvCategory { get; set; }
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")]
[FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")]
public string TvImportedCategory { get; set; }
[FieldDefinition(8, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTransmissionSettingsDirectoryHelpText")]
public string TvDirectory { get; set; }
[FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")]
[FieldDefinition(9, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsRecentPriorityEpisodeHelpText")]
public int RecentTvPriority { get; set; }
[FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")]
[FieldDefinition(10, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")]
public int OlderTvPriority { get; set; }
[FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
[FieldDefinition(11, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public override NzbDroneValidationResult Validate()

View File

@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Transmission
@@ -11,6 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public long TotalSize { get; set; }
public long LeftUntilDone { get; set; }
public bool IsFinished { get; set; }
public IReadOnlyCollection<string> Labels { get; set; } = Array.Empty<string>();
public long Eta { get; set; }
public TransmissionTorrentStatus Status { get; set; }
public long SecondsDownloading { get; set; }

View File

@@ -15,6 +15,9 @@ namespace NzbDrone.Core.Download.Clients.Vuze
{
private const int MINIMUM_SUPPORTED_PROTOCOL_VERSION = 14;
public override string Name => "Vuze";
public override bool SupportsLabels => false;
public Vuze(ITransmissionProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
@@ -67,7 +70,5 @@ namespace NzbDrone.Core.Download.Clients.Vuze
return null;
}
public override string Name => "Vuze";
}
}

View File

@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Plex
episodeFormat = $"SP{episodesInFile.First():00}";
}
content.Append($"Episode: {episodeFormat}: {episodeFile.RelativePath}");
content.AppendLine($"Episode: {episodeFormat}: {episodeFile.RelativePath}");
}
}

View File

@@ -237,6 +237,7 @@
"CollectionsLoadError": "Unable to load collections",
"ColonReplacement": "Colon Replacement",
"ColonReplacementFormatHelpText": "Change how {appName} handles colon replacement",
"Completed": "Completed",
"CompletedDownloadHandling": "Completed Download Handling",
"Component": "Component",
"Condition": "Condition",
@@ -302,6 +303,7 @@
"CustomFormatsSpecificationResolution": "Resolution",
"CustomFormatsSpecificationSource": "Source",
"Cutoff": "Cutoff",
"CutoffNotMet": "Cutoff Not Met",
"CutoffUnmet": "Cutoff Unmet",
"CutoffUnmetLoadError": "Error loading cutoff unmet items",
"CutoffUnmetNoItems": "No cutoff unmet items",
@@ -542,8 +544,8 @@
"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}'",
"DownloadClientUnavailable": "Download Client Unavailable",
"DownloadClientUTorrentTorrentStateError": "uTorrent is reporting an error",
"DownloadClientUnavailable": "Download Client Unavailable",
"DownloadClientValidationApiKeyIncorrect": "API Key Incorrect",
"DownloadClientValidationApiKeyRequired": "API Key Required",
"DownloadClientValidationAuthenticationFailure": "Authentication Failure",
@@ -1155,6 +1157,7 @@
"MediaManagementSettingsSummary": "Naming, file management settings and root folders",
"Medium": "Medium",
"MegabytesPerMinute": "Megabytes Per Minute",
"Menu": "Menu",
"Message": "Message",
"Metadata": "Metadata",
"MetadataLoadError": "Unable to load Metadata",
@@ -1433,6 +1436,7 @@
"NotificationsSettingsWebhookMethod": "Method",
"NotificationsSettingsWebhookMethodHelpText": "Which HTTP method to use submit to the Webservice",
"NotificationsSettingsWebhookUrl": "Webhook URL",
"NotificationsSettingsWebhookHeaders": "Headers",
"NotificationsSignalSettingsGroupIdPhoneNumber": "Group ID / Phone Number",
"NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Group ID / Phone Number of the receiver",
"NotificationsSignalSettingsPasswordHelpText": "Password used to authenticate requests toward signal-api",
@@ -1575,6 +1579,7 @@
"PreferredProtocol": "Preferred Protocol",
"PreferredSize": "Preferred Size",
"PrefixedRange": "Prefixed Range",
"Premiere": "Premiere",
"Presets": "Presets",
"PreviewRename": "Preview Rename",
"PreviewRenameSeason": "Preview Rename for this season",

View File

@@ -2122,5 +2122,13 @@
"FolderNameTokens": "Tokens de nombre de carpeta",
"FailedToFetchSettings": "Error al recuperar la configuración",
"MetadataPlexSettingsEpisodeMappings": "Asignaciones de episodios",
"MetadataPlexSettingsEpisodeMappingsHelpText": "Incluye las asignaciones de episodios para todos los archivos en el archivo .plexmatch"
"MetadataPlexSettingsEpisodeMappingsHelpText": "Incluye las asignaciones de episodios para todos los archivos en el archivo .plexmatch",
"RecentFolders": "Carpetas recientes",
"Warning": "Aviso",
"Delay": "Retardo",
"DownloadClientUnavailable": "Cliente de descarga no disponible",
"FavoriteFolders": "Carpetas favoritas",
"ManageFormats": "Gestionar formatos",
"FavoriteFolderAdd": "Añadir carpeta favorita",
"FavoriteFolderRemove": "Eliminar carpeta favorita"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"Backup": "Copie de rezervă",
"CloneCondition": "Clonați condiție",
"CloneCondition": "Clonează condiție",
"ApplyChanges": "Aplicați modificări",
"AutomaticAdd": "Adăugare automată",
"AllTitles": "Toate titlurile",
@@ -21,7 +21,7 @@
"ApplyTagsHelpTextReplace": "Înlocuire: înlocuiți etichetele cu etichetele introduse (nu introduceți etichete pentru a șterge toate etichetele)",
"CancelPendingTask": "Sigur doriți să anulați această sarcină în așteptare?",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nu pot comunica cu {downloadClientName}. {errorMessage}",
"CloneCustomFormat": "Clonați format personalizat",
"CloneCustomFormat": "Clonează format personalizat",
"Close": "Închide",
"Delete": "Șterge",
"Added": "Adăugat",
@@ -205,5 +205,9 @@
"OnFileImport": "La import fișier",
"FileNameTokens": "Jetoane pentru nume de fișier",
"FolderNameTokens": "Jetoane pentru nume de folder",
"OnFileUpgrade": "La actualizare fișier"
"OnFileUpgrade": "La actualizare fișier",
"CloneIndexer": "Clonează Indexer",
"CloneProfile": "Clonează Profil",
"DownloadClientUnavailable": "Client de descărcare indisponibil",
"Clone": "Clonează"
}

View File

@@ -878,5 +878,26 @@
"Install": "Kur",
"InstallMajorVersionUpdate": "Güncellemeyi Kur",
"InstallMajorVersionUpdateMessage": "Bu güncelleştirme yeni bir ana sürüm yükleyecek ve sisteminizle uyumlu olmayabilir. Bu güncelleştirmeyi yüklemek istediğinizden emin misiniz?",
"InstallMajorVersionUpdateMessageLink": "Daha fazla bilgi için lütfen [{domain}]({url}) adresini kontrol edin."
"InstallMajorVersionUpdateMessageLink": "Daha fazla bilgi için lütfen [{domain}]({url}) adresini kontrol edin.",
"AnimeEpisodeTypeDescription": "Kesin bölüm numarası kullanılarak yayınlanan bölümler",
"AnimeEpisodeTypeFormat": "Kesin bölüm numarası ({format})",
"Warning": "Uyarı",
"AirsTimeOn": "{time} {networkLabel} üzerinde",
"AirsTomorrowOn": "Yarın {time}'da {networkLabel}'da",
"AppUpdatedVersion": "{appName} `{version}` sürümüne güncellendi, en son değişiklikleri almak için {appName} uygulamasını yeniden başlatmanız gerekiyor ",
"ApplyTags": "Etiketleri Uygula",
"AnalyseVideoFiles": "Video dosyalarını analiz edin",
"Anime": "Anime",
"AnimeEpisodeFormat": "Anime Bölüm Formatı",
"AudioInfo": "Ses Bilgisi",
"AuthBasic": "Temel (Tarayıcıılır Penceresi)",
"ApplyTagsHelpTextHowToApplySeries": "Seçili serilere etiketler nasıl uygulanır?",
"AptUpdater": "Güncellemeyi yüklemek için apt'ı kullanın",
"AnalyseVideoFilesHelpText": "Çözünürlük, çalışma zamanı ve kodek bilgileri gibi video bilgilerini dosyalardan çıkarın. Bu, {appName} uygulamasının taramalar sırasında yüksek disk veya ağ etkinliğine neden olabilecek dosyanın bölümlerini okumasını gerektirir.",
"Delay": "Gecikme",
"Fallback": "Geri Çek",
"ManageFormats": "Biçimleri Yönet",
"FavoriteFolderAdd": "Favori Klasör Ekle",
"FavoriteFolderRemove": "Favori Klasörü Kaldır",
"FavoriteFolders": "Favori Klasörler"
}

View File

@@ -1953,5 +1953,6 @@
"InstallMajorVersionUpdateMessageLink": "请查看 [{domain}]({url}) 以获取更多信息。",
"Install": "安装",
"InstallMajorVersionUpdate": "安装更新",
"InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?"
"InstallMajorVersionUpdateMessage": "此更新将安装新的主要版本,这可能与您的系统不兼容。您确定要安装此更新吗?",
"Fallback": "备选"
}

View File

@@ -567,7 +567,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
_logger.ProgressTrace("Manually imported {0} files", imported.Count);
}
var untrackedImports = imported.Where(i => importedTrackedDownload.FirstOrDefault(t => t.ImportResult != i) == null).ToList();
var untrackedImports = imported.Where(i => i.Result == ImportResultType.Imported && importedTrackedDownload.FirstOrDefault(t => t.ImportResult != i) == null).ToList();
if (untrackedImports.Any())
{

View File

@@ -43,6 +43,11 @@ namespace NzbDrone.Core.Notifications.Webhook
request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password);
}
foreach (var header in settings.Headers)
{
request.Headers.Add(header.Key, header.Value);
}
_httpClient.Execute(request);
}
catch (HttpException ex)

View File

@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
@@ -20,6 +21,7 @@ namespace NzbDrone.Core.Notifications.Webhook
public WebhookSettings()
{
Method = Convert.ToInt32(WebhookMethod.POST);
Headers = new List<KeyValuePair<string, string>>();
}
[FieldDefinition(0, Label = "NotificationsSettingsWebhookUrl", Type = FieldType.Url)]
@@ -34,6 +36,9 @@ namespace NzbDrone.Core.Notifications.Webhook
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "NotificationsSettingsWebhookHeaders", Type = FieldType.KeyValueList, Advanced = true)]
public IEnumerable<KeyValuePair<string, string>> Headers { get; set; }
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));

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>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH)(?:\W|_))|(?<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)",
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>german\b|videomann|ger[. ]dub)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH)(?:\W|_))|(?<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)|(?<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))(?:(?i)(?![\W|_|^]SUB))",
@@ -470,6 +470,11 @@ namespace NzbDrone.Core.Parser
{
languages.Add(Language.Turkish);
}
if (match.Groups["original"].Success)
{
languages.Add(Language.Original);
}
}
return languages;

View File

@@ -564,7 +564,7 @@ namespace NzbDrone.Core.Parser
// 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 MAN)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
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)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled);

View File

@@ -5931,8 +5931,21 @@
"name": "quality",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
},
{
"name": "status",
"in": "query",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QueueStatus"
}
}
}
],
@@ -10855,8 +10868,7 @@
"nullable": true
},
"status": {
"type": "string",
"nullable": true
"$ref": "#/components/schemas/QueueStatus"
},
"trackedDownloadStatus": {
"$ref": "#/components/schemas/TrackedDownloadStatus"
@@ -10935,6 +10947,21 @@
},
"additionalProperties": false
},
"QueueStatus": {
"enum": [
"unknown",
"queued",
"paused",
"downloading",
"completed",
"failed",
"warning",
"delay",
"downloadClientUnavailable",
"fallback"
],
"type": "string"
},
"QueueStatusResource": {
"type": "object",
"properties": {