mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Compare commits
31 Commits
no-logging
...
auth-succe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98c737a146 | ||
|
|
f0798550af | ||
|
|
d9c7838329 | ||
|
|
b00229e53c | ||
|
|
880628fb68 | ||
|
|
b09c6f0811 | ||
|
|
b376b63c9e | ||
|
|
99feaa34d2 | ||
|
|
d7f82a72c2 | ||
|
|
bd20ebfad7 | ||
|
|
71553ad67b | ||
|
|
41c39f1f28 | ||
|
|
d0066358eb | ||
|
|
6f1d461dad | ||
|
|
6ccab3cfc8 | ||
|
|
5e47cc3baa | ||
|
|
78ca30d1f8 | ||
|
|
f9d0abada3 | ||
|
|
4bdb0408f1 | ||
|
|
40ea6ce4e5 | ||
|
|
ccf33033dc | ||
|
|
996c0e9f50 | ||
|
|
8b7f9daab0 | ||
|
|
dfb6fdfbeb | ||
|
|
29d0073ee6 | ||
|
|
9cf6be32fa | ||
|
|
fee3f8150e | ||
|
|
010bbbd222 | ||
|
|
d3c3a6ebce | ||
|
|
f26344ae75 | ||
|
|
034f731308 |
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -18,5 +18,6 @@ export default interface MediaManagement {
|
||||
scriptImportPath: string;
|
||||
importExtraFiles: boolean;
|
||||
extraFileExtensions: string;
|
||||
userRejectedExtensions: string;
|
||||
enableMediaInfo: boolean;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class nzb_su_url_to_nzb_lifeFixture : MigrationTest<nzb_su_url_to_nzb_life>
|
||||
{
|
||||
[TestCase("Newznab", "https://api.nzb.su")]
|
||||
[TestCase("Newznab", "http://api.nzb.su")]
|
||||
public void should_replace_old_url(string impl, string baseUrl)
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Indexers").Row(new
|
||||
{
|
||||
Name = "Nzb.su",
|
||||
Implementation = impl,
|
||||
Settings = new NewznabSettings219
|
||||
{
|
||||
BaseUrl = baseUrl,
|
||||
ApiPath = "/api"
|
||||
}.ToJson(),
|
||||
ConfigContract = impl + "Settings",
|
||||
EnableInteractiveSearch = false
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl.Replace("su", "life"));
|
||||
}
|
||||
|
||||
[TestCase("Newznab", "https://api.indexer.com")]
|
||||
public void should_not_replace_different_url(string impl, string baseUrl)
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("Indexers").Row(new
|
||||
{
|
||||
Name = "Indexer.com",
|
||||
Implementation = impl,
|
||||
Settings = new NewznabSettings219
|
||||
{
|
||||
BaseUrl = baseUrl,
|
||||
ApiPath = "/api"
|
||||
}.ToJson(),
|
||||
ConfigContract = impl + "Settings",
|
||||
EnableInteractiveSearch = false
|
||||
});
|
||||
});
|
||||
|
||||
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
internal class IndexerDefinition219
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public JObject Settings { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableAutomaticSearch { get; set; }
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public int DownloadClientId { get; set; }
|
||||
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||
}
|
||||
|
||||
internal class NewznabSettings219
|
||||
{
|
||||
public string BaseUrl { get; set; }
|
||||
public string ApiPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
public void should_return_season_time_for_season_packs()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -145,6 +145,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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
19
src/NzbDrone.Core.Test/ParserTests/SubGroupParserFixture.cs
Normal file
19
src/NzbDrone.Core.Test/ParserTests/SubGroupParserFixture.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(219)]
|
||||
public class nzb_su_url_to_nzb_life : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.Sql("UPDATE \"Indexers\" SET \"Settings\" = replace(\"Settings\", '//api.nzb.su', '//api.nzb.life')" +
|
||||
"WHERE \"Implementation\" = 'Newznab'" +
|
||||
"AND \"Settings\" LIKE '%//api.nzb.su%'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(229)]
|
||||
public class enable_season_pack_seeding_goal : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(SetSeasonPackSeedingGoal);
|
||||
}
|
||||
|
||||
private void SetSeasonPackSeedingGoal(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updatedIndexers = new List<object>();
|
||||
|
||||
using var selectCommand = conn.CreateCommand();
|
||||
|
||||
selectCommand.Transaction = tran;
|
||||
selectCommand.CommandText = "SELECT * FROM \"Indexers\"";
|
||||
|
||||
using var reader = selectCommand.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
var idIndex = reader.GetOrdinal("Id");
|
||||
var settingsIndex = reader.GetOrdinal("Settings");
|
||||
|
||||
var id = reader.GetInt32(idIndex);
|
||||
var settings = Json.Deserialize<Dictionary<string, object>>(reader.GetString(settingsIndex));
|
||||
|
||||
if (settings.TryGetValue("seedCriteria", out var seedCriteriaToken) && seedCriteriaToken is JObject seedCriteria)
|
||||
{
|
||||
if (seedCriteria?["seasonPackSeedTime"] != null)
|
||||
{
|
||||
seedCriteria["seasonPackSeedGoal"] = 1;
|
||||
|
||||
if (seedCriteria["seedRatio"] != null)
|
||||
{
|
||||
seedCriteria["seasonPackSeedRatio"] = seedCriteria["seedRatio"];
|
||||
}
|
||||
|
||||
updatedIndexers.Add(new
|
||||
{
|
||||
Settings = settings.ToJson(),
|
||||
Id = id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedIndexers.Any())
|
||||
{
|
||||
var updateSql = "UPDATE \"Indexers\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateSql, updatedIndexers, transaction: tran);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Tribler
|
||||
{
|
||||
public enum DownloadStatus
|
||||
{
|
||||
[EnumMember(Value = @"WAITING4HASHCHECK")]
|
||||
Waiting4HashCheck = 0,
|
||||
|
||||
[EnumMember(Value = @"HASHCHECKING")]
|
||||
Hashchecking = 1,
|
||||
|
||||
[EnumMember(Value = @"METADATA")]
|
||||
Metadata = 2,
|
||||
|
||||
[EnumMember(Value = @"DOWNLOADING")]
|
||||
Downloading = 3,
|
||||
|
||||
[EnumMember(Value = @"SEEDING")]
|
||||
Seeding = 4,
|
||||
|
||||
[EnumMember(Value = @"STOPPED")]
|
||||
Stopped = 5,
|
||||
|
||||
[EnumMember(Value = @"ALLOCATING_DISKSPACE")]
|
||||
AllocatingDiskspace = 6,
|
||||
|
||||
[EnumMember(Value = @"EXIT_NODES")]
|
||||
Exitnodes = 7,
|
||||
|
||||
[EnumMember(Value = @"CIRCUITS")]
|
||||
Circuits = 8,
|
||||
|
||||
[EnumMember(Value = @"STOPPED_ON_ERROR")]
|
||||
StoppedOnError = 9,
|
||||
|
||||
[EnumMember(Value = @"LOADING")]
|
||||
Loading = 10,
|
||||
}
|
||||
|
||||
public class Trackers
|
||||
{
|
||||
public string Url { get; set; }
|
||||
[JsonProperty("peers")]
|
||||
public object Peers { get; set; }
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
}
|
||||
|
||||
public class Download
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public float? Progress { get; set; }
|
||||
public string Infohash { get; set; }
|
||||
public bool? AnonDownload { get; set; }
|
||||
public float? Availability { get; set; }
|
||||
public double? Eta { get; set; }
|
||||
public long? TotalPieces { get; set; }
|
||||
public long? NumSeeds { get; set; }
|
||||
public long? AllTimeUpload { get; set; }
|
||||
public long? AllTimeDownload { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public DownloadStatus? Status { get; set; }
|
||||
|
||||
public int? StatusCode { get; set; }
|
||||
public float? AllTimeRatio { get; set; }
|
||||
public long? TimeAdded { get; set; }
|
||||
public long? MaxUploadSpeed { get; set; }
|
||||
public long? MaxDownloadSpeed { get; set; }
|
||||
public long? Hops { get; set; }
|
||||
public bool? SafeSeeding { get; set; }
|
||||
public string Error { get; set; }
|
||||
public long? TotalDown { get; set; }
|
||||
public long? Size { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public float? SpeedDown { get; set; }
|
||||
public float? SpeedUp { get; set; }
|
||||
public long? NumPeers { get; set; }
|
||||
public List<Trackers> Trackers { get; set; }
|
||||
}
|
||||
|
||||
public class DownloadsResponse
|
||||
{
|
||||
public List<Download> Downloads { get; set; }
|
||||
}
|
||||
|
||||
public class AddDownloadRequest
|
||||
{
|
||||
[JsonProperty("anon_hops")]
|
||||
public long? AnonymityHops { get; set; }
|
||||
|
||||
[JsonProperty("safe_seeding")]
|
||||
public bool? SafeSeeding { get; set; }
|
||||
public string Destination { get; set; }
|
||||
|
||||
[JsonProperty("uri", Required = Newtonsoft.Json.Required.Always)]
|
||||
[Required(AllowEmptyStrings = true)]
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
|
||||
public class AddDownloadResponse
|
||||
{
|
||||
public string Infohash { get; set; }
|
||||
public bool? Started { get; set; }
|
||||
}
|
||||
|
||||
public class RemoveDownloadRequest
|
||||
{
|
||||
[JsonProperty("remove_data")]
|
||||
public bool? RemoveData { get; set; }
|
||||
}
|
||||
|
||||
public class DeleteDownloadResponse
|
||||
{
|
||||
public bool? Removed { get; set; }
|
||||
public string Infohash { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateDownloadRequest
|
||||
{
|
||||
[JsonProperty("anon_hops")]
|
||||
public long? AnonHops { get; set; }
|
||||
|
||||
[JsonProperty("selected_files")]
|
||||
public List<int> Selected_files { get; set; }
|
||||
|
||||
public string State { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateDownloadResponse
|
||||
{
|
||||
public bool? Modified { get; set; }
|
||||
public string Infohash { get; set; }
|
||||
}
|
||||
|
||||
public class File
|
||||
{
|
||||
public long? Size { get; set; }
|
||||
public long? Index { get; set; }
|
||||
public string Name { get; set; }
|
||||
public float? Progress { get; set; }
|
||||
public bool? Included { get; set; }
|
||||
}
|
||||
|
||||
public class GetFilesResponse
|
||||
{
|
||||
public List<File> Files { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Tribler
|
||||
{
|
||||
public class TriblerSettingsResponse
|
||||
{
|
||||
public Settings Settings { get; set; }
|
||||
}
|
||||
|
||||
public class Settings
|
||||
{
|
||||
public Api Api { get; set; }
|
||||
public bool Statistics { get; set; }
|
||||
|
||||
[JsonProperty("content_discovery_community")]
|
||||
public ContentDiscoveryCommunity ContentDiscoveryCommunity { get; set; }
|
||||
public Database Database { get; set; }
|
||||
|
||||
[JsonProperty("dht_discovery")]
|
||||
public DHTDiscovery DHTDiscovery { get; set; }
|
||||
|
||||
[JsonProperty("knowledge_community")]
|
||||
public KnowledgeCommunity KnowledgeCommunity { get; set; }
|
||||
public LibTorrent LibTorrent { get; set; }
|
||||
public Recommender Recommender { get; set; }
|
||||
public Rendezvous RecoRendezvousmmender { get; set; }
|
||||
|
||||
[JsonProperty("torrent_checker")]
|
||||
public TorrentChecker TorrentChecker { get; set; }
|
||||
|
||||
[JsonProperty("tunnel_community")]
|
||||
public TunnelCommunity TunnelCommunity { get; set; }
|
||||
|
||||
public Versioning Versioning { get; set; }
|
||||
|
||||
[JsonProperty("watch_folder")]
|
||||
public WatchFolder WatchFolder { get; set; }
|
||||
|
||||
[JsonProperty("state_dir")]
|
||||
public string StateDir { get; set; }
|
||||
|
||||
[JsonProperty("memory_db")]
|
||||
public bool? MemoryDB { get; set; }
|
||||
}
|
||||
|
||||
public class Api
|
||||
{
|
||||
[JsonProperty("http_enabled")]
|
||||
public bool HttpEnabled { get; set; }
|
||||
|
||||
[JsonProperty("http_port")]
|
||||
public int HttpPort { get; set; }
|
||||
|
||||
[JsonProperty("http_host")]
|
||||
public string HttpHost { get; set; }
|
||||
|
||||
[JsonProperty("https_enabled")]
|
||||
public bool HttpsEnabled { get; set; }
|
||||
|
||||
[JsonProperty("https_port")]
|
||||
public int HttpsPort { get; set; }
|
||||
|
||||
[JsonProperty("https_host")]
|
||||
public string HttpsHost { get; set; }
|
||||
|
||||
[JsonProperty("https_certfile")]
|
||||
public string HttpsCertFile { get; set; }
|
||||
|
||||
[JsonProperty("http_port_running")]
|
||||
public int HttpPortRunning { get; set; }
|
||||
|
||||
[JsonProperty("https_port_running")]
|
||||
public int HttpsPortRunning { get; set; }
|
||||
}
|
||||
|
||||
public class ContentDiscoveryCommunity
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class Database
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class DHTDiscovery
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class KnowledgeCommunity
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class LibTorrent
|
||||
{
|
||||
[JsonProperty("download_defaults")]
|
||||
public LibTorrentDownloadDefaults DownloadDefaults { get; set; }
|
||||
|
||||
// contains a lot more data, but it's not needed currently
|
||||
}
|
||||
|
||||
public class Recommender
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class Rendezvous
|
||||
{
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class TorrentChecker
|
||||
{
|
||||
[JsonProperty("enabled")]
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class TunnelCommunity
|
||||
{
|
||||
[JsonProperty("enabled")]
|
||||
public bool? Enabled { get; set; }
|
||||
|
||||
[JsonProperty("min_circuits")]
|
||||
public int? MinCircuits { get; set; }
|
||||
|
||||
[JsonProperty("max_circuits")]
|
||||
public int? MaxCircuits { get; set; }
|
||||
}
|
||||
|
||||
public class Versioning
|
||||
{
|
||||
[JsonProperty("enabled")]
|
||||
public bool? Enabled { get; set; }
|
||||
}
|
||||
|
||||
public class WatchFolder
|
||||
{
|
||||
[JsonProperty("enabled")]
|
||||
public bool? Enabled { get; set; }
|
||||
[JsonProperty("directory")]
|
||||
public string Directory { get; set; }
|
||||
[JsonProperty("check_interval")]
|
||||
public int? CheckInterval { get; set; }
|
||||
}
|
||||
|
||||
public class LibTorrentDownloadDefaults
|
||||
{
|
||||
[JsonProperty("anonymity_enabled")]
|
||||
public bool? AnonymityEnabled { get; set; }
|
||||
|
||||
[JsonProperty("number_hops")]
|
||||
public int? NumberHops { get; set; }
|
||||
|
||||
[JsonProperty("safeseeding_enabled")]
|
||||
public bool? SafeSeedingEnabled { get; set; }
|
||||
|
||||
[JsonProperty("saveas")]
|
||||
public string SaveAS { get; set; }
|
||||
|
||||
[JsonProperty("seeding_mode")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public DownloadDefaultsSeedingMode? SeedingMode { get; set; }
|
||||
|
||||
[JsonProperty("seeding_ratio")]
|
||||
public double? SeedingRatio { get; set; }
|
||||
|
||||
[JsonProperty("seeding_time")]
|
||||
public double? SeedingTime { get; set; }
|
||||
}
|
||||
|
||||
public enum DownloadDefaultsSeedingMode
|
||||
{
|
||||
[EnumMember(Value = @"ratio")]
|
||||
Ratio = 0,
|
||||
|
||||
[EnumMember(Value = @"forever")]
|
||||
Forever = 1,
|
||||
|
||||
[EnumMember(Value = @"time")]
|
||||
Time = 2,
|
||||
|
||||
[EnumMember(Value = @"never")]
|
||||
Never = 3,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Tribler;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.RemotePathMappings;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Tribler
|
||||
{
|
||||
public class TriblerDownloadClient : TorrentClientBase<TriblerDownloadSettings>
|
||||
{
|
||||
private readonly ITriblerDownloadClientProxy _proxy;
|
||||
|
||||
public TriblerDownloadClient(
|
||||
ITriblerDownloadClientProxy triblerDownloadClientProxy,
|
||||
ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
ILocalizationService localizationService,
|
||||
IBlocklistService blocklistService,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger)
|
||||
{
|
||||
_proxy = triblerDownloadClientProxy;
|
||||
}
|
||||
|
||||
public override string Name => "Tribler";
|
||||
|
||||
public override ProviderMessage Message => new ProviderMessage(_localizationService.GetLocalizedString("DownloadClientTriblerProviderMessage", new Dictionary<string, object> { { "clientName", Name }, { "clientVersionRange", "8.0.7" } }), ProviderMessageType.Warning);
|
||||
|
||||
public override bool PreferTorrentFile => false;
|
||||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
var configAsync = _proxy.GetConfig(Settings);
|
||||
|
||||
var items = new List<DownloadClientItem>();
|
||||
|
||||
var downloads = _proxy.GetDownloads(Settings);
|
||||
|
||||
foreach (var download in downloads)
|
||||
{
|
||||
// If totalsize == 0 the torrent is a magnet downloading metadata
|
||||
if (download.Size == null || download.Size == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem
|
||||
{
|
||||
DownloadId = download.Infohash,
|
||||
Title = download.Name,
|
||||
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false)
|
||||
};
|
||||
|
||||
// some concurrency could make this faster.
|
||||
var files = _proxy.GetDownloadFiles(Settings, download);
|
||||
|
||||
item.OutputPath = new OsPath(download.Destination);
|
||||
|
||||
if (files.Count == 1)
|
||||
{
|
||||
item.OutputPath += files.First().Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.OutputPath += item.Title;
|
||||
}
|
||||
|
||||
item.TotalSize = (long)download.Size;
|
||||
item.RemainingSize = (long)(download.Size * (1 - download.Progress));
|
||||
item.SeedRatio = download.AllTimeRatio;
|
||||
|
||||
if (download.Eta.HasValue)
|
||||
{
|
||||
if (download.Eta.Value >= TimeSpan.FromDays(365).TotalSeconds)
|
||||
{
|
||||
item.RemainingTime = TimeSpan.FromDays(365);
|
||||
}
|
||||
else if (download.Eta.Value < 0)
|
||||
{
|
||||
item.RemainingTime = TimeSpan.FromSeconds(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.RemainingTime = TimeSpan.FromSeconds(download.Eta.Value);
|
||||
}
|
||||
}
|
||||
|
||||
item.Message = download.Error;
|
||||
|
||||
// tribler always saves files unencrypted to disk.
|
||||
item.IsEncrypted = false;
|
||||
|
||||
switch (download.Status)
|
||||
{
|
||||
case DownloadStatus.Hashchecking:
|
||||
case DownloadStatus.Waiting4HashCheck:
|
||||
case DownloadStatus.Circuits:
|
||||
case DownloadStatus.Exitnodes:
|
||||
case DownloadStatus.Downloading:
|
||||
item.Status = DownloadItemStatus.Downloading;
|
||||
break;
|
||||
case DownloadStatus.Metadata:
|
||||
case DownloadStatus.AllocatingDiskspace:
|
||||
item.Status = DownloadItemStatus.Queued;
|
||||
break;
|
||||
case DownloadStatus.Seeding:
|
||||
case DownloadStatus.Stopped:
|
||||
item.Status = DownloadItemStatus.Completed;
|
||||
break;
|
||||
case DownloadStatus.StoppedOnError:
|
||||
item.Status = DownloadItemStatus.Failed;
|
||||
break;
|
||||
case DownloadStatus.Loading:
|
||||
default: // new status in API? default to downloading
|
||||
item.Message = "Unknown download state: " + download.Status;
|
||||
_logger.Info(item.Message);
|
||||
item.Status = DownloadItemStatus.Downloading;
|
||||
break;
|
||||
}
|
||||
|
||||
// Override status if completed, but not finished downloading
|
||||
if (download.Status == DownloadStatus.Stopped && download.Progress < 1)
|
||||
{
|
||||
item.Status = DownloadItemStatus.Paused;
|
||||
}
|
||||
|
||||
if (download.Error != null && download.Error.Length > 0)
|
||||
{
|
||||
item.Status = DownloadItemStatus.Warning;
|
||||
item.Message = download.Error;
|
||||
}
|
||||
|
||||
item.CanBeRemoved = item.CanMoveFiles = HasReachedSeedLimit(download, configAsync);
|
||||
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public override void RemoveItem(DownloadClientItem item, bool deleteData)
|
||||
{
|
||||
_proxy.RemoveDownload(Settings, item, deleteData);
|
||||
}
|
||||
|
||||
public override DownloadClientInfo GetStatus()
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var destDir = config.Settings.LibTorrent.DownloadDefaults.SaveAS;
|
||||
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
destDir = string.Format("{0}/.{1}", destDir, Settings.TvCategory);
|
||||
}
|
||||
|
||||
return new DownloadClientInfo
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
|
||||
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) }
|
||||
};
|
||||
}
|
||||
|
||||
protected static bool HasReachedSeedLimit(Download torrent, TriblerSettingsResponse config)
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
if (torrent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(torrent));
|
||||
}
|
||||
|
||||
// if download is still running then it's not finished.
|
||||
if (torrent.Status != DownloadStatus.Stopped)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (config.Settings.LibTorrent.DownloadDefaults.SeedingMode)
|
||||
{
|
||||
case DownloadDefaultsSeedingMode.Ratio:
|
||||
|
||||
return torrent.AllTimeRatio.HasValue
|
||||
&& torrent.AllTimeRatio >= config.Settings.LibTorrent.DownloadDefaults.SeedingRatio;
|
||||
|
||||
case DownloadDefaultsSeedingMode.Time:
|
||||
var downloadStarted = DateTimeOffset.FromUnixTimeSeconds(torrent.TimeAdded.Value);
|
||||
var maxSeedingTime = TimeSpan.FromSeconds(config.Settings.LibTorrent.DownloadDefaults.SeedingTime ?? 0);
|
||||
|
||||
return torrent.TimeAdded.HasValue
|
||||
&& downloadStarted.Add(maxSeedingTime) < DateTimeOffset.Now;
|
||||
|
||||
case DownloadDefaultsSeedingMode.Never:
|
||||
return true;
|
||||
|
||||
case DownloadDefaultsSeedingMode.Forever:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
||||
{
|
||||
var addDownloadRequestObject = new AddDownloadRequest
|
||||
{
|
||||
Destination = GetDownloadDirectory(),
|
||||
Uri = magnetLink,
|
||||
SafeSeeding = Settings.SafeSeeding,
|
||||
AnonymityHops = Settings.AnonymityLevel
|
||||
};
|
||||
|
||||
return _proxy.AddFromMagnetLink(Settings, addDownloadRequestObject);
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
// TODO: Tribler 8.x does support adding from a torrent file, but it's not a simple put command.
|
||||
throw new NotSupportedException("Tribler does not support torrent files, only magnet links");
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
|
||||
if (failures.HasErrors())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected string GetDownloadDirectory()
|
||||
{
|
||||
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return Settings.TvDirectory;
|
||||
}
|
||||
|
||||
if (!Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var destDir = config.Settings.LibTorrent.DownloadDefaults.SaveAS;
|
||||
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
|
||||
}
|
||||
|
||||
protected ValidationFailure TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
var downloads = GetItems();
|
||||
return null;
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
_logger.Error(ex, ex.Message);
|
||||
|
||||
return new ValidationFailure("ApiKey", _localizationService.GetLocalizedString("DownloadClientValidationApiKeyIncorrect"));
|
||||
}
|
||||
catch (DownloadClientUnavailableException ex)
|
||||
{
|
||||
_logger.Error(ex, ex.Message);
|
||||
|
||||
return new NzbDroneValidationFailure("Host", _localizationService.GetLocalizedString("DownloadClientValidationUnableToConnect", new Dictionary<string, object> { { "clientName", Name } }))
|
||||
{
|
||||
DetailedDescription = ex.Message
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to test");
|
||||
|
||||
return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientValidationUnknownException", new Dictionary<string, object> { { "exception", ex.Message } }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Tribler;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Tribler
|
||||
{
|
||||
public interface ITriblerDownloadClientProxy
|
||||
{
|
||||
List<Download> GetDownloads(TriblerDownloadSettings settings);
|
||||
List<File> GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem);
|
||||
TriblerSettingsResponse GetConfig(TriblerDownloadSettings settings);
|
||||
void RemoveDownload(TriblerDownloadSettings settings, DownloadClientItem item, bool deleteData);
|
||||
string AddFromMagnetLink(TriblerDownloadSettings settings, AddDownloadRequest downloadRequest);
|
||||
}
|
||||
|
||||
public class TriblerDownloadClientProxy : ITriblerDownloadClientProxy
|
||||
{
|
||||
protected readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public TriblerDownloadClientProxy(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private HttpRequestBuilder GetRequestBuilder(TriblerDownloadSettings settings, string relativePath = null)
|
||||
{
|
||||
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
|
||||
baseUrl = HttpUri.CombinePath(baseUrl, relativePath);
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(baseUrl)
|
||||
.Accept(HttpAccept.Json);
|
||||
|
||||
requestBuilder.Headers.Add("X-Api-Key", settings.ApiKey);
|
||||
requestBuilder.LogResponseContent = true;
|
||||
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
private T ProcessRequest<T>(HttpRequestBuilder requestBuilder)
|
||||
where T : new()
|
||||
{
|
||||
return ProcessRequest<T>(requestBuilder.Build());
|
||||
}
|
||||
|
||||
private T ProcessRequest<T>(HttpRequest requestBuilder)
|
||||
where T : new()
|
||||
{
|
||||
var httpRequest = requestBuilder;
|
||||
|
||||
_logger.Debug("Url: {0}", httpRequest.Url);
|
||||
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Execute(httpRequest);
|
||||
return Json.Deserialize<T>(response.Content);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Unauthorized - AuthToken is invalid", ex);
|
||||
}
|
||||
|
||||
throw new DownloadClientUnavailableException("Unable to connect to Tribler. Status Code: {0}", ex.Response.StatusCode, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public TriblerSettingsResponse GetConfig(TriblerDownloadSettings settings)
|
||||
{
|
||||
var configRequest = GetRequestBuilder(settings, "api/settings");
|
||||
return ProcessRequest<TriblerSettingsResponse>(configRequest);
|
||||
}
|
||||
|
||||
public List<File> GetDownloadFiles(TriblerDownloadSettings settings, Download downloadItem)
|
||||
{
|
||||
var filesRequest = GetRequestBuilder(settings, "api/downloads/" + downloadItem.Infohash + "/files");
|
||||
return ProcessRequest<GetFilesResponse>(filesRequest).Files;
|
||||
}
|
||||
|
||||
public List<Download> GetDownloads(TriblerDownloadSettings settings)
|
||||
{
|
||||
var downloadRequest = GetRequestBuilder(settings, "api/downloads");
|
||||
var downloads = ProcessRequest<DownloadsResponse>(downloadRequest);
|
||||
return downloads.Downloads;
|
||||
}
|
||||
|
||||
public void RemoveDownload(TriblerDownloadSettings settings, DownloadClientItem item, bool deleteData)
|
||||
{
|
||||
var deleteDownloadRequestObject = new RemoveDownloadRequest
|
||||
{
|
||||
RemoveData = deleteData
|
||||
};
|
||||
|
||||
var deleteRequestBuilder = GetRequestBuilder(settings, "api/downloads/" + item.DownloadId.ToLower());
|
||||
deleteRequestBuilder.Method = HttpMethod.Delete;
|
||||
|
||||
var deleteRequest = deleteRequestBuilder.Build();
|
||||
deleteRequest.SetContent(Json.ToJson(deleteDownloadRequestObject));
|
||||
|
||||
ProcessRequest<DeleteDownloadResponse>(deleteRequest);
|
||||
}
|
||||
|
||||
public string AddFromMagnetLink(TriblerDownloadSettings settings, AddDownloadRequest downloadRequest)
|
||||
{
|
||||
var addDownloadRequestBuilder = GetRequestBuilder(settings, "api/downloads");
|
||||
addDownloadRequestBuilder.Method = HttpMethod.Put;
|
||||
|
||||
var addDownloadRequest = addDownloadRequestBuilder.Build();
|
||||
addDownloadRequest.SetContent(Json.ToJson(downloadRequest));
|
||||
|
||||
return ProcessRequest<AddDownloadResponse>(addDownloadRequest).Infohash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Tribler
|
||||
{
|
||||
public class TriblerSettingsValidator : AbstractValidator<TriblerDownloadSettings>
|
||||
{
|
||||
public TriblerSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).ValidHost();
|
||||
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||
RuleFor(c => c.UrlBase).ValidUrlBase();
|
||||
RuleFor(c => c.ApiKey).NotEmpty();
|
||||
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
|
||||
RuleFor(c => c.TvCategory).Empty()
|
||||
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use Category and Directory");
|
||||
RuleFor(c => c.AnonymityLevel).GreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class TriblerDownloadSettings : IProviderConfig
|
||||
{
|
||||
private static readonly TriblerSettingsValidator Validator = new TriblerSettingsValidator();
|
||||
|
||||
public TriblerDownloadSettings()
|
||||
{
|
||||
Host = "localhost";
|
||||
Port = 20100;
|
||||
UrlBase = "";
|
||||
AnonymityLevel = 1;
|
||||
SafeSeeding = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Host", Type = FieldType.Textbox)]
|
||||
public string Host { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Port", Type = FieldType.Textbox)]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "UseSsl", Type = FieldType.Checkbox, HelpText = "DownloadClientSettingsUseSslHelpText")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "UrlBase", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsUrlBaseHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "clientName", "Tribler")]
|
||||
[FieldToken(TokenField.HelpText, "UrlBase", "url", "http://[host]:[port]/[urlBase]")]
|
||||
public string UrlBase { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, HelpText = "DownloadClientTriblerSettingsApiKeyHelpText")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")]
|
||||
public string TvCategory { get; set; }
|
||||
|
||||
[FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientTriblerSettingsDirectoryHelpText")]
|
||||
public string TvDirectory { get; set; }
|
||||
|
||||
[FieldDefinition(8, Label = "DownloadClientTriblerSettingsAnonymityLevel", Type = FieldType.Number, HelpText = "DownloadClientTriblerSettingsAnonymityLevelHelpText")]
|
||||
[FieldToken(TokenField.HelpText, "DownloadClientTriblerSettingsAnonymityLevel", "url", "https://www.tribler.org/anonymity.html")]
|
||||
public int AnonymityLevel { get; set; }
|
||||
|
||||
[FieldDefinition(9, Label = "DownloadClientTriblerSettingsSafeSeeding", Type = FieldType.Checkbox, HelpText = "DownloadClientTriblerSettingsSafeSeedingHelpText")]
|
||||
public bool SafeSeeding { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,12 @@ public class RejectedImportService : IRejectedImportService
|
||||
_logger.Trace("Download '{0}' contains executable file, marking as failed", trackedDownload.DownloadItem.Title);
|
||||
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));
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -8,5 +8,8 @@ public enum FailDownloads
|
||||
Executables = 0,
|
||||
|
||||
[FieldOption(Label = "Potentially Dangerous")]
|
||||
PotentiallyDangerous = 1
|
||||
PotentiallyDangerous = 1,
|
||||
|
||||
[FieldOption(Label = "User Defined Extensions")]
|
||||
UserDefinedExtensions = 2
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr"));
|
||||
yield return GetDefinition("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"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
11
src/NzbDrone.Core/Indexers/SeasonPackSeedGoal.cs
Normal file
11
src/NzbDrone.Core/Indexers/SeasonPackSeedGoal.cs
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
try
|
||||
{
|
||||
return MagnetLink.Parse(magnetUrl).InfoHash.ToHex();
|
||||
return MagnetLink.Parse(magnetUrl).InfoHashes.V1OrV2.ToHex();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
@@ -2161,5 +2161,15 @@
|
||||
"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+)"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -2163,5 +2163,13 @@
|
||||
"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)"
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
@@ -2156,10 +2156,20 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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 d’un 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",
|
||||
@@ -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,45 @@
|
||||
"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",
|
||||
"DownloadClientUTorrentProviderMessage": "uTorrent a l'habitude d'inclure des cryptomineurs, des logiciels malveillants et des publicités, nous vous encourageons fortement à choisir un client différent.",
|
||||
"DownloadClientQbittorrentSettingsAddSeriesTags": "Ajouter des tags de séries",
|
||||
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Ajouter des tags de séries aux nouveaux torrents ajoutés au client de téléchargement (qBittorrent 4.1.0+)",
|
||||
"FavoriteFolders": "Dossier favori",
|
||||
"MinimumCustomFormatScoreIncrementHelpText": "Amélioration minimale requise du score de format personnalisé entre les versions existantes et nouvelles avant que {appName} ne le considère comme une mise à niveau",
|
||||
"MonitorEpisodesModalInfo": "Ce paramètre n'ajustera que les épisodes ou saisons qui seront surveillés dans une série. Sélectionner Aucun retirera la surveillance de la série",
|
||||
"NotificationsTelegramSettingsIncludeInstanceName": "Inclure le nom de l'instance dans le titre",
|
||||
"NotificationsPushcutSettingsMetadataLinks": "Lien de métadonnées",
|
||||
"UserRejectedExtensions": "Extensions de fichiers rejetées supplémentaires",
|
||||
"UserRejectedExtensionsHelpText": "Liste séparée par des virgules des extensions de fichiers à échouer (“Échouer les téléchargements” doit également être activé dans l’indexeur)",
|
||||
"UserRejectedExtensionsTextsExamples": "Examples : '.ext, .xyz' or 'ext,xyz'",
|
||||
"Warning": "Avertissement"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -2163,5 +2163,13 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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ă"
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
"AnimeEpisodeFormat": "Формат аниме-эпизода",
|
||||
"AuthBasic": "Базовый (Всплывающее окно браузера)",
|
||||
"AuthForm": "Формы (Страница авторизации)",
|
||||
"Authentication": "Авторизация",
|
||||
"Authentication": "Аутентификация",
|
||||
"AuthenticationRequired": "Требуется авторизация",
|
||||
"BackupIntervalHelpText": "Периодичность автоматического резервного копирования",
|
||||
"BackupRetentionHelpText": "Автоматические резервные копии старше указанного периода будут автоматически удалены",
|
||||
@@ -1500,7 +1500,7 @@
|
||||
"RejectionCount": "Количество отказов",
|
||||
"Release": "Релиз",
|
||||
"ReleaseGroup": "Релиз группа",
|
||||
"ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).`).",
|
||||
"ReleaseGroupFootNote": "При необходимости можно управлять обрезкой до максимального количества байтов, включая многоточие (`...`). Поддерживается обрезка как с конца (например, `{Release Group:30}`), так и с начала (например, `{Release Group:-30}`).",
|
||||
"ReleaseProfileIndexerHelpText": "Укажите, к какому индексатору применяется профиль",
|
||||
"ReleaseProfileIndexerHelpTextWarning": "Установка определенного индексатора в профиле релиза приведет к тому, что этот профиль будет применяться только к релизам из этого индексатора.",
|
||||
"ReleaseProfiles": "Профили релизов",
|
||||
@@ -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": "Спец. эпизод",
|
||||
@@ -2162,5 +2162,14 @@
|
||||
"DownloadClientItemErrorMessage": "{clientName} сообщает об ошибке: {message}",
|
||||
"AutoTaggingSpecificationNetwork": "Сеть(и)",
|
||||
"NotificationsAppriseSettingsIncludePoster": "Добавить постер",
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение"
|
||||
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение",
|
||||
"EpisodeMonitoring": "Отслеживание эпизода",
|
||||
"MonitorEpisodes": "Отслеживать эпизоды",
|
||||
"MonitorEpisodesModalInfo": "Эта настройка влияет только на отслеживание эпизодов или сезонов внутри сериала. Выбор ничего приведёт к остановке отслеживания сериала",
|
||||
"ImportListsSimklSettingsUserListTypeHold": "Оставить",
|
||||
"UserRejectedExtensions": "Дополнительные запрещенные расширения файлов",
|
||||
"UserRejectedExtensionsTextsExamples": "Примеры: '.ext, .xyz' или 'ext,xyz'",
|
||||
"DownloadClientQbittorrentSettingsAddSeriesTags": "Добавлять теги сериалов",
|
||||
"DownloadClientQbittorrentSettingsAddSeriesTagsHelpText": "Добавлять теги сериалов к новым торрентам, добавляемым в загрузчик (qBittorrent 4.1.0+)",
|
||||
"UserRejectedExtensionsHelpText": "Список запрещенных расширений файлов, разделенных запятой (так же нужно включить настройку Считать загрузки неуспешными в настройках индексаторов)"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -1791,7 +1791,7 @@
|
||||
"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",
|
||||
@@ -2163,5 +2163,13 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,7 @@ public enum ImportRejectionReason
|
||||
UnknownSeries,
|
||||
DangerousFile,
|
||||
ExecutableFile,
|
||||
UserRejectedExtension,
|
||||
ArchiveFile,
|
||||
SeriesFolder,
|
||||
InvalidFilePath,
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.TorrentInfo
|
||||
{
|
||||
try
|
||||
{
|
||||
return Torrent.Load(fileContents).InfoHash.ToHex();
|
||||
return Torrent.Load(fileContents).InfoHashes.V1OrV2.ToHex();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -136,6 +136,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
|
||||
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeAirDatesUtc", string.Join(",", episodeFile.Episodes.Value.Select(e => e.AirDateUtc)));
|
||||
environmentVariables.Add("Sonarr_EpisodeFile_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)));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(@"(?:\W|_)(?<english>\b(?:ing|eng)\b)|(?<italian>\b(?:ita|italian)\b)|(?<german>(?:swiss)?german\b|videomann|ger[. ]dub|\bger\b)|(?<flemish>flemish)|(?<greek>greek)|(?<french>(?:\W|_|\b)(?:FR|VF|VF2|VFF|VFI|VFQ|TRUEFRENCH|FRENCH|FRE|FRA)(?:\W|_|\b))|(?<russian>\b(?:rus|ru)\b)|(?<hungarian>\b(?:HUNDUB|HUN)\b)|(?<hebrew>\bHebDub\b)|(?<polish>\b(?:PL\W?DUB|DUB\W?PL|LEK\W?PL|PL\W?LEK)\b)|(?<chinese>\[(?:CH[ST]|BIG5|GB)\]|简|繁|字幕)|(?<bulgarian>\bbgaudio\b)|(?<spanish>\b(?:español|castellano|esp|spa(?!\(Latino\)))\b)|(?<ukrainian>\b(?:\dx?)?(?:ukr))|(?<thai>\b(?:THAI)\b)|(?<romanian>\b(?:RoDubbed|ROMANIAN)\b)|(?<catalan>[-,. ]cat[. ](?:DD|subs)|\b(?:catalan|catalán)\b)|(?<latvian>\b(?:lat|lav|lv)\b)|(?<turkish>\b(?:tur)\b)|(?<urdu>\burdu\b)|(?<romansh>\b(?:romansh|rumantsch|romansch)\b)|(?<japanese>\(JA\))|(?<original>\b(?:orig|original)\b)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex CaseSensitiveLanguageRegex = new Regex(@"(?:(?i)(?<!SUB[\W|_|^]))(?:(?<lithuanian>\bLT\b)|(?<czech>\bCZ\b)|(?<polish>\bPL\b)|(?<bulgarian>\bBG\b)|(?<slovak>\bSK\b)|(?<german>\bDE\b))(?:(?i)(?![\W|_|^]SUB))",
|
||||
@@ -496,6 +496,11 @@ namespace NzbDrone.Core.Parser
|
||||
languages.Add(Language.Romansh);
|
||||
}
|
||||
|
||||
if (match.Groups["japanese"].Success)
|
||||
{
|
||||
languages.Add(Language.Japanese);
|
||||
}
|
||||
|
||||
if (match.Groups["original"].Success)
|
||||
{
|
||||
languages.Add(Language.Original);
|
||||
|
||||
@@ -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)))
|
||||
{
|
||||
|
||||
59
src/NzbDrone.Core/Parser/ParserCommon.cs
Normal file
59
src/NzbDrone.Core/Parser/ParserCommon.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
87
src/NzbDrone.Core/Parser/ReleaseGroupParser.cs
Normal file
87
src/NzbDrone.Core/Parser/ReleaseGroupParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||
<PackageReference Include="MonoTorrent" Version="3.0.2" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Tv
|
||||
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
|
||||
Episode FindEpisode(int seriesId, string date, int? part);
|
||||
List<Episode> GetEpisodeBySeries(int seriesId);
|
||||
List<Episode> GetEpisodesBySeries(List<int> seriesIds);
|
||||
List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber);
|
||||
List<Episode> GetEpisodesBySceneSeason(int seriesId, int sceneSeasonNumber);
|
||||
List<Episode> EpisodesWithFiles(int seriesId);
|
||||
@@ -99,6 +100,11 @@ namespace NzbDrone.Core.Tv
|
||||
return _episodeRepository.GetEpisodes(seriesId).ToList();
|
||||
}
|
||||
|
||||
public List<Episode> GetEpisodesBySeries(List<int> seriesIds)
|
||||
{
|
||||
return _episodeRepository.GetEpisodesBySeriesIds(seriesIds).ToList();
|
||||
}
|
||||
|
||||
public List<Episode> GetEpisodesBySeason(int seriesId, int seasonNumber)
|
||||
{
|
||||
return _episodeRepository.GetEpisodes(seriesId, seasonNumber);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace NzbDrone.Host
|
||||
b.ClearProviders();
|
||||
b.SetMinimumLevel(LogLevel.Trace);
|
||||
b.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
|
||||
b.AddFilter("Sonarr.Http.Authentication", LogLevel.Information);
|
||||
b.AddFilter("Sonarr.Http.Authentication.ApiKeyAuthenticationHandler", LogLevel.Information);
|
||||
b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error);
|
||||
b.AddNLog();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
using Sonarr.Http;
|
||||
@@ -37,6 +40,28 @@ namespace Sonarr.Api.V3.Config
|
||||
SharedValidator.RuleFor(c => c.ScriptImportPath).IsValidPath().When(c => c.UseScriptImport);
|
||||
|
||||
SharedValidator.RuleFor(c => c.MinimumFreeSpaceWhenImporting).GreaterThanOrEqualTo(100);
|
||||
|
||||
SharedValidator.RuleFor(c => c.UserRejectedExtensions).Custom((extensions, context) =>
|
||||
{
|
||||
var userRejectedExtensions = extensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(e => e.Trim(' ', '.')
|
||||
.Insert(0, "."))
|
||||
.ToList();
|
||||
|
||||
var matchingArchiveExtensions = userRejectedExtensions.Where(ext => FileExtensions.ArchiveExtensions.Contains(ext)).ToList();
|
||||
|
||||
if (matchingArchiveExtensions.Count > 0)
|
||||
{
|
||||
context.AddFailure($"Rejected extensions may not include valid archive extensions: {string.Join(", ", matchingArchiveExtensions)}");
|
||||
}
|
||||
|
||||
var matchingMediaFileExtensions = userRejectedExtensions.Where(ext => MediaFileExtensions.Extensions.Contains(ext)).ToList();
|
||||
|
||||
if (matchingMediaFileExtensions.Count > 0)
|
||||
{
|
||||
context.AddFailure($"Rejected extensions may not include valid media file extensions: {string.Join(", ", matchingMediaFileExtensions)}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override MediaManagementConfigResource ToResource(IConfigService model)
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Sonarr.Api.V3.Config
|
||||
public bool ImportExtraFiles { get; set; }
|
||||
public string ExtraFileExtensions { get; set; }
|
||||
public bool EnableMediaInfo { get; set; }
|
||||
public string UserRejectedExtensions { get; set; }
|
||||
}
|
||||
|
||||
public static class MediaManagementConfigResourceMapper
|
||||
@@ -59,7 +60,8 @@ namespace Sonarr.Api.V3.Config
|
||||
ScriptImportPath = model.ScriptImportPath,
|
||||
ImportExtraFiles = model.ImportExtraFiles,
|
||||
ExtraFileExtensions = model.ExtraFileExtensions,
|
||||
EnableMediaInfo = model.EnableMediaInfo
|
||||
EnableMediaInfo = model.EnableMediaInfo,
|
||||
UserRejectedExtensions = model.UserRejectedExtensions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V3.Episodes
|
||||
{
|
||||
@@ -26,5 +28,22 @@ namespace Sonarr.Api.V3.Episodes
|
||||
|
||||
return _renameEpisodeFileService.GetRenamePreviews(seriesId).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet("bulk")]
|
||||
[Produces("application/json")]
|
||||
public List<RenameEpisodeResource> GetEpisodes([FromQuery] List<int> seriesIds)
|
||||
{
|
||||
if (seriesIds is { Count: 0 })
|
||||
{
|
||||
throw new BadRequestException("seriesIds must be provided");
|
||||
}
|
||||
|
||||
if (seriesIds.Any(seriesId => seriesId <= 0))
|
||||
{
|
||||
throw new BadRequestException("seriesIds must be positive integers");
|
||||
}
|
||||
|
||||
return _renameEpisodeFileService.GetRenamePreviews(seriesIds).ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Core.Qualities;
|
||||
using Sonarr.Api.V3.CustomFormats;
|
||||
using Sonarr.Api.V3.Episodes;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V3.ManualImport
|
||||
{
|
||||
@@ -37,6 +38,11 @@ namespace Sonarr.Api.V3.ManualImport
|
||||
[Consumes("application/json")]
|
||||
public object ReprocessItems([FromBody] List<ManualImportReprocessResource> items)
|
||||
{
|
||||
if (items is { Count: 0 })
|
||||
{
|
||||
throw new BadRequestException("items must be provided");
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags, item.ReleaseType);
|
||||
|
||||
@@ -35,6 +35,8 @@ public class SeriesResource : RestResource
|
||||
public int TvRageId { get; set; }
|
||||
public int TvMazeId { get; set; }
|
||||
public int TmdbId { get; set; }
|
||||
public HashSet<int>? MalIds { get; set; }
|
||||
public HashSet<int>? AniListIds { get; set; }
|
||||
public DateTime? FirstAired { get; set; }
|
||||
public DateTime? LastAired { get; set; }
|
||||
public SeriesTypes SeriesType { get; set; }
|
||||
@@ -81,6 +83,8 @@ public static class SeriesResourceMapper
|
||||
TvRageId = model.TvRageId,
|
||||
TvMazeId = model.TvMazeId,
|
||||
TmdbId = model.TmdbId,
|
||||
MalIds = model.MalIds,
|
||||
AniListIds = model.AniListIds,
|
||||
FirstAired = model.FirstAired,
|
||||
LastAired = model.LastAired,
|
||||
SeriesType = model.SeriesType,
|
||||
@@ -122,6 +126,8 @@ public static class SeriesResourceMapper
|
||||
TvRageId = resource.TvRageId,
|
||||
TvMazeId = resource.TvMazeId,
|
||||
TmdbId = resource.TmdbId,
|
||||
MalIds = resource.MalIds,
|
||||
AniListIds = resource.AniListIds,
|
||||
FirstAired = resource.FirstAired,
|
||||
SeriesType = resource.SeriesType,
|
||||
CleanTitle = resource.CleanTitle,
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Sonarr.Http.Authentication
|
||||
|
||||
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddOptions<CookieAuthenticationOptions>(AuthenticationType.Forms.ToString())
|
||||
services.AddOptions<CookieAuthenticationOptions>(nameof(AuthenticationType.Forms))
|
||||
.Configure<IConfigFileProvider>((options, configFileProvider) =>
|
||||
{
|
||||
// Replace diacritics and replace non-word characters to ensure cookie name doesn't contain any valid URL characters not allowed in cookie names
|
||||
@@ -47,12 +47,9 @@ namespace Sonarr.Http.Authentication
|
||||
});
|
||||
|
||||
return services.AddAuthentication()
|
||||
.AddNone(AuthenticationType.None.ToString())
|
||||
.AddExternal(AuthenticationType.External.ToString())
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
.AddCookie(AuthenticationType.Basic.ToString())
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
.AddCookie(AuthenticationType.Forms.ToString())
|
||||
.AddNone(nameof(AuthenticationType.None))
|
||||
.AddExternal(nameof(AuthenticationType.External))
|
||||
.AddCookie(nameof(AuthenticationType.Forms))
|
||||
.AddApiKey("API", options =>
|
||||
{
|
||||
options.HeaderName = "X-Api-Key";
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Sonarr.Http.Authentication
|
||||
|
||||
private void LogSuccess(HttpRequest context, string username)
|
||||
{
|
||||
_authLogger.Info("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username);
|
||||
_authLogger.Debug("Auth-Success ip {0} username '{1}'", context.GetRemoteIP(), username);
|
||||
}
|
||||
|
||||
private void LogLogout(HttpRequest context, string username)
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Http.Authentication
|
||||
{
|
||||
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
||||
{
|
||||
private const string POLICY_NAME = "UI";
|
||||
private const string PolicyName = "UI";
|
||||
private readonly IConfigFileProvider _config;
|
||||
|
||||
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
|
||||
@@ -26,7 +26,7 @@ namespace NzbDrone.Http.Authentication
|
||||
|
||||
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
|
||||
{
|
||||
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase))
|
||||
if (policyName.Equals(PolicyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
|
||||
.AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement());
|
||||
|
||||
Reference in New Issue
Block a user