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

Compare commits

...

19 Commits

Author SHA1 Message Date
Mark McDowall
627b2a4289 New: Parse 480i Bluray/Remux as Bluray 480p
Closes #6801
2024-05-09 22:04:18 -07:00
Bogdan
9734c2d144 Fixed: Notifications with only On Rename enabled
ignore-downstream
2024-05-09 22:04:12 -07:00
Bogdan
c7c1e3ac9e Refactor PasswordInput to use type password 2024-05-09 22:04:04 -07:00
Bogdan
429444d085 Fixed: Text color for inputs on login page 2024-05-09 22:03:56 -07:00
Mark McDowall
5cb649e9d8 Fixed: Attempt to parse and reject ambiguous dates
Closes #6799
2024-05-09 22:03:44 -07:00
Mark McDowall
cac7d239ea Fixed: Parsing of partial season pack 2024-05-09 22:03:44 -07:00
Sonarr
3940059ea3 Automated API Docs update
ignore-downstream
2024-05-09 22:03:31 -07:00
Weblate
20d00fe88c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-05-09 22:03:24 -07:00
Mark McDowall
b4d05214ae Fixed: Ignore invalid movie tags when writing XBMC metadata
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-05-08 18:45:19 -07:00
Mark McDowall
cc0a284660 New: Add series tags to Webhook and Notifiarr events 2024-05-08 18:45:19 -07:00
Mark McDowall
f50a263f4f New: Add Custom Format Score to file in Episode Details 2024-05-08 18:45:03 -07:00
Mark McDowall
29176c8367 New: Has Unmonitored Season filter for Series 2024-05-08 18:44:52 -07:00
Bogdan
1eddf3a152 Use number input for seed ratio 2024-05-08 18:44:36 -07:00
Bogdan
8360dd7a7b Fixed: Parsing long downloading/seeding values from Transmission 2024-05-08 18:44:27 -07:00
Bogdan
7e8d8500f2 Fixed: Next/previous/last air dates with Postgres DB
Closes #6790
2024-05-08 18:43:51 -07:00
Mark McDowall
cae134ec7b New: Dark theme for login screen
Closes #6751
2024-05-08 18:42:54 -07:00
Stevie Robinson
f81bb3ec19 New: Blocklist Custom Filters
Closes #6763
2024-05-08 18:42:41 -07:00
Bogdan
128309068d Fixed: Initialize databases after app folder migrations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-05-08 18:42:15 -07:00
Mickaël Thomas
73a4bdea52 New: Support stoppedUP and stoppedDL states from qBittorrent 2024-05-08 18:41:59 -07:00
60 changed files with 981 additions and 357 deletions

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -20,6 +21,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlocklistFilterModal from './BlocklistFilterModal';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
@@ -114,9 +116,13 @@ class Blocklist extends Component {
error,
items,
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onFilterSelect,
...otherProps
} = this.props;
@@ -161,6 +167,15 @@ class Blocklist extends Component {
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={BlocklistFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
@@ -180,7 +195,11 @@ class Blocklist extends Component {
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoHistoryBlocklist')}
{
selectedFilterKey === 'all' ?
translate('NoHistoryBlocklist') :
translate('BlocklistFilterHasNoItems')
}
</Alert>
}
@@ -251,11 +270,15 @@ Blocklist.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
onClearBlocklistPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
@@ -13,10 +14,12 @@ import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCustomFiltersSelector('blocklist'),
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
(blocklist, customFilters, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
customFilters,
...blocklist
};
}
@@ -97,6 +100,10 @@ class BlocklistConnector extends Component {
this.props.setBlocklistSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setBlocklistFilter({ selectedFilterKey });
};
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
};
@@ -122,6 +129,7 @@ class BlocklistConnector extends Component {
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
@@ -142,6 +150,7 @@ BlocklistConnector.propTypes = {
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistFilter: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
function createBlocklistSelector() {
return createSelector(
(state: AppState) => state.blocklist.items,
(blocklistItems) => {
return blocklistItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.blocklist.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface BlocklistFilterModalProps {
isOpen: boolean;
}
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
const sectionItems = useSelector(createBlocklistSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'blocklist';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setBlocklistFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -12,11 +12,10 @@ function App({ store, history }) {
<DocumentTitle title={window.Sonarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
<ApplyTheme />
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View File

@@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Sonarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View File

@@ -0,0 +1,37 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
Object.entries(themes[theme]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
export default ApplyTheme;

View File

@@ -1,4 +1,5 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import EpisodeFilesAppState from './EpisodeFilesAppState';
@@ -54,6 +55,7 @@ export interface AppSectionState {
interface AppState {
app: AppSectionState;
blocklist: BlocklistAppState;
calendar: CalendarAppState;
commands: CommandAppState;
episodeFiles: EpisodeFilesAppState;

View File

@@ -0,0 +1,8 @@
import Blocklist from 'typings/Blocklist';
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
interface BlocklistAppState
extends AppSectionState<Blocklist>,
AppSectionFilterState<Blocklist> {}
export default BlocklistAppState;

View File

@@ -1,5 +0,0 @@
.input {
composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}

View File

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

View File

@@ -1,7 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
@@ -13,17 +11,14 @@ function PasswordInput(props) {
return (
<TextInput
{...props}
type="password"
onCopy={onCopy}
/>
);
}
PasswordInput.propTypes = {
className: PropTypes.string.isRequired
};
PasswordInput.defaultProps = {
className: styles.input
...TextInput.props
};
export default PasswordInput;

View File

@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
}
/*
* text-security-disc
*/
@font-face {
font-weight: normal;
font-style: normal;
font-family: 'text-security-disc';
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
}

View File

@@ -17,6 +17,12 @@
width: 175px;
}
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 65px;
}
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'customFormatScore': string;
'customFormats': string;
'languages': string;
'quality': string;

View File

@@ -11,6 +11,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import MediaInfo from './MediaInfo';
import styles from './EpisodeFileRow.css';
@@ -55,6 +56,7 @@ class EpisodeFileRow extends Component {
languages,
quality,
customFormats,
customFormatScore,
qualityCutoffNotMet,
mediaInfo,
columns
@@ -127,6 +129,17 @@ class EpisodeFileRow extends Component {
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
@@ -183,6 +196,7 @@ EpisodeFileRow.propTypes = {
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeleteEpisodeFile: PropTypes.func.isRequired

View File

@@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds, sizes } from 'Helpers/Props';
import { icons, kinds, sizes } from 'Helpers/Props';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import translate from 'Utilities/String/translate';
import EpisodeAiringConnector from './EpisodeAiringConnector';
@@ -42,6 +43,15 @@ const columns = [
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'actions',
label: '',
@@ -94,6 +104,7 @@ class EpisodeSummary extends Component {
languages,
quality,
customFormats,
customFormatScore,
qualityCutoffNotMet,
onDeleteEpisodeFile
} = this.props;
@@ -143,6 +154,7 @@ class EpisodeSummary extends Component {
quality={quality}
qualityCutoffNotMet={qualityCutoffNotMet}
customFormats={customFormats}
customFormatScore={customFormatScore}
mediaInfo={mediaInfo}
columns={columns}
onDeleteEpisodeFile={onDeleteEpisodeFile}
@@ -179,6 +191,7 @@ EpisodeSummary.propTypes = {
quality: PropTypes.object,
qualityCutoffNotMet: PropTypes.bool,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
onDeleteEpisodeFile: PropTypes.func.isRequired
};

View File

@@ -31,7 +31,8 @@ function createMapStateToProps() {
languages,
quality,
qualityCutoffNotMet,
customFormats
customFormats,
customFormatScore
} = episodeFile;
return {
@@ -45,7 +46,8 @@ function createMapStateToProps() {
languages,
quality,
qualityCutoffNotMet,
customFormats
customFormats,
customFormatScore
};
}
);

View File

@@ -1,6 +1,6 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -77,6 +77,31 @@ export const defaultState = {
isVisible: true,
isModifiable: false
}
],
selectedFilterKey: 'all',
filters: [
{
key: 'all',
label: () => translate('All'),
filters: []
}
],
filterBuilderProps: [
{
name: 'seriesIds',
label: () => translate('Series'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.SERIES
},
{
name: 'protocols',
label: () => translate('Protocol'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.PROTOCOL
}
]
};
@@ -84,6 +109,7 @@ export const persistState = [
'blocklist.pageSize',
'blocklist.sortKey',
'blocklist.sortDirection',
'blocklist.selectedFilterKey',
'blocklist.columns'
];
@@ -97,6 +123,7 @@ export const GOTO_NEXT_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistNextPage';
export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage';
export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage';
export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort';
export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter';
export const SET_BLOCKLIST_TABLE_OPTION = 'blocklist/setBlocklistTableOption';
export const REMOVE_BLOCKLIST_ITEM = 'blocklist/removeBlocklistItem';
export const REMOVE_BLOCKLIST_ITEMS = 'blocklist/removeBlocklistItems';
@@ -112,6 +139,7 @@ export const gotoBlocklistNextPage = createThunk(GOTO_NEXT_BLOCKLIST_PAGE);
export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE);
export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE);
export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT);
export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER);
export const setBlocklistTableOption = createAction(SET_BLOCKLIST_TABLE_OPTION);
export const removeBlocklistItem = createThunk(REMOVE_BLOCKLIST_ITEM);
export const removeBlocklistItems = createThunk(REMOVE_BLOCKLIST_ITEMS);
@@ -132,7 +160,8 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT,
[serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER
}),
[REMOVE_BLOCKLIST_ITEM]: createRemoveItemHandler(section, '/blocklist'),

View File

@@ -192,6 +192,22 @@ export const filterPredicates = {
});
return predicate(hasMissingSeason, filterValue);
},
hasUnmonitoredSeason: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { seasons = [] } = item;
const hasUnmonitoredSeason = seasons.some((season) => {
const {
seasonNumber,
monitored
} = season;
return seasonNumber > 0 && !monitored;
});
return predicate(hasUnmonitoredSeason, filterValue);
}
};
@@ -353,6 +369,12 @@ export const filterBuilderProps = [
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'hasUnmonitoredSeason',
label: () => translate('HasUnmonitoredSeason'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'year',
label: () => translate('Year'),

View File

@@ -2,7 +2,7 @@ import * as dark from './dark';
import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light };
const auto = defaultDark ? dark : light;
export default {
auto,

View File

@@ -2,7 +2,6 @@ module.exports = {
// Families
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
passwordFamily: 'text-security-disc',
// Sizes
extraSmallFontSize: '11px',

View File

@@ -57,8 +57,8 @@
<style>
body {
background-color: #f5f7fa;
color: #656565;
background-color: var(--pageBackground);
color: var(--textColor);
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
sans-serif;
}
@@ -88,14 +88,14 @@
padding: 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background-color: #3a3f51;
background-color: var(--themeDarkColor);
}
.panel-body {
padding: 20px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
background-color: var(--panelBackground);
}
.sign-in {
@@ -112,16 +112,18 @@
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid #dde6e9;
background-color: var(--inputBackgroundColor);
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
color: var(--textColor);
}
.form-input:focus {
outline: 0;
border-color: #66afe9;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(102, 175, 233, 0.6);
border-color: var(--inputFocusBorderColor);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
0 0 8px var(--inputFocusBoxShadowColor);
}
.button {
@@ -130,10 +132,10 @@
padding: 10px 0;
width: 100%;
border: 1px solid;
border-color: #5899eb;
border-color: var(--primaryBorderColor);
border-radius: 4px;
background-color: #5d9cec;
color: #fff;
background-color: var(--primaryBackgroundColor);
color: var(--white);
vertical-align: middle;
text-align: center;
white-space: nowrap;
@@ -141,9 +143,9 @@
}
.button:hover {
border-color: #3483e7;
background-color: #4b91ea;
color: #fff;
border-color: var(--primaryHoverBorderColor);
background-color: var(--primaryHoverBackgroundColor);
color: var(--white);
text-decoration: none;
}
@@ -165,24 +167,24 @@
.forgot-password {
margin-left: auto;
color: #909fa7;
color: var(--forgotPasswordColor);
text-decoration: none;
font-size: 13px;
}
.forgot-password:focus,
.forgot-password:hover {
color: #748690;
color: var(--forgotPasswordAltColor);
text-decoration: underline;
}
.forgot-password:visited {
color: #748690;
color: var(--forgotPasswordAltColor);
}
.login-failed {
margin-top: 20px;
color: #f05050;
color: var(--failedColor);
font-size: 14px;
}
@@ -291,5 +293,59 @@
loginFailedDiv.classList.remove("hidden");
}
var light = {
white: '#fff',
pageBackground: '#f5f7fa',
textColor: '#515253',
themeDarkColor: '#3a3f51',
panelBackground: '#fff',
inputBackgroundColor: '#fff',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#909fa7',
forgotPasswordAltColor: '#748690'
};
var dark = {
white: '#fff',
pageBackground: '#202020',
textColor: '#ccc',
themeDarkColor: '#494949',
panelBackground: '#111',
inputBackgroundColor: '#333',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#737d83',
forgotPasswordAltColor: '#546067'
};
var theme = "_THEME_";
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
dark :
light;
Object.entries(finalTheme).forEach(([key, value]) => {
document.documentElement.style.setProperty(
`--${key}`,
value
);
});
</script>
</html>

View File

@@ -0,0 +1,16 @@
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
interface Blocklist extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
title: string;
date?: string;
protocol: string;
seriesId?: number;
}
export default Blocklist;

View File

@@ -1,5 +1,5 @@
export interface UiSettings {
theme: string;
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;

View File

@@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
VerifyWarning(item);
}
[Test]
public void paused_item_should_have_required_properties()
[TestCase("pausedDL")]
[TestCase("stoppedDL")]
public void paused_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
@@ -188,7 +189,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedDL",
State = state,
Label = "",
SavePath = ""
};
@@ -200,6 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
}
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
[TestCase("queuedUP")]
[TestCase("uploading")]
[TestCase("stalledUP")]
@@ -418,8 +420,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
}
[Test]
public void api_261_should_use_content_path()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void api_261_should_use_content_path(string state)
{
var torrent = new QBittorrentTorrent
{
@@ -428,7 +431,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedUP",
State = state,
Label = "",
SavePath = @"C:\Torrents".AsOsAgnostic(),
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
@@ -657,44 +660,48 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set(string state)
{
GivenGlobalSeedLimits(-1);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused(string state)
{
GivenGlobalSeedLimits(1.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f);
GivenCompletedTorrent(state, ratio: 1.0f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f);
GivenCompletedTorrent(state, ratio: 1.0f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused(string state)
{
GivenGlobalSeedLimits(0.2f);
GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f);
GivenCompletedTorrent(state, ratio: 0.5f, ratioLimit: 0.8f);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -712,33 +719,36 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -756,66 +766,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
GivenCompletedTorrent(state, ratio: 2.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_overridden_max_inactive_seedingtime_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 40);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20, inactiveSeedingTimeLimit: 10, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(15)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_be_removable_if_overridden_max_inactive_seedingtime_not_reached_and_paused(string state)
{
GivenGlobalSeedLimits(-1, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 30, inactiveSeedingTimeLimit: 40, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(30)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
item.CanMoveFiles.Should().BeFalse();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f, 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30);
GivenCompletedTorrent(state, ratio: 1.0f, seedingTime: 30);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_be_removable_and_should_allow_move_files_if_max_inactive_seedingtime_reached_but_ratio_not_and_paused(string state)
{
GivenGlobalSeedLimits(2.0f, maxInactiveSeedingTime: 20);
GivenCompletedTorrent("pausedUP", ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
GivenCompletedTorrent(state, ratio: 1.0f, lastActivity: DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(25)).ToUnixTimeSeconds());
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeTrue();
item.CanMoveFiles.Should().BeTrue();
}
[Test]
public void should_not_fetch_details_twice()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_not_fetch_details_twice(string state)
{
GivenGlobalSeedLimits(-1, 30);
GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20);
GivenCompletedTorrent(state, ratio: 2.0f, seedingTime: 20);
var item = Subject.GetItems().Single();
item.CanBeRemoved.Should().BeFalse();
@@ -827,8 +843,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
.Verify(p => p.GetTorrentProperties(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once());
}
[Test]
public void should_get_category_from_the_category_if_set()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_category_if_set(string state)
{
const string category = "tv-sonarr";
GivenGlobalSeedLimits(1.0f);
@@ -840,7 +857,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
State = state,
Category = category,
SavePath = "",
Ratio = 1.0f
@@ -852,8 +869,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
item.Category.Should().Be(category);
}
[Test]
public void should_get_category_from_the_label_if_the_category_is_not_available()
[TestCase("pausedUP")]
[TestCase("stoppedUP")]
public void should_get_category_from_the_label_if_the_category_is_not_available(string state)
{
const string category = "tv-sonarr";
GivenGlobalSeedLimits(1.0f);
@@ -865,7 +883,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "pausedUP",
State = state,
Label = category,
SavePath = "",
Ratio = 1.0f

View File

@@ -33,6 +33,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("2019_08_20_1080_all.mp4", "", 2019, 8, 20)]
[TestCase("Series and Title 20201013 Ep7432 [720p WebRip (x264)] [SUBS]", "Series and Title", 2020, 10, 13)]
[TestCase("Series Title (1955) - 1954-01-23 05 00 00 - Cottage for Sale.ts", "Series Title (1955)", 1954, 1, 23)]
[TestCase("Series Title - 30-04-2024 HDTV 1080p H264 AAC", "Series Title", 2024, 4, 30)]
// [TestCase("", "", 0, 0, 0)]
public void should_parse_daily_episode(string postTitle, string title, int year, int month, int day)
@@ -100,5 +101,13 @@ namespace NzbDrone.Core.Test.ParserTests
Parser.Parser.ParseTitle(title).Should().BeNull();
}
[TestCase("Tmc - Quotidien - 05-06-2024 HDTV 1080p H264 AAC")]
// [TestCase("", "", 0, 0, 0)]
public void should_not_parse_ambiguous_daily_episode(string postTitle)
{
Parser.Parser.ParseTitle(postTitle).Should().BeNull();
}
}
}

View File

@@ -109,6 +109,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Series.S01E05.480p.BluRay.DD5.1.x264-HiSD", false)]
[TestCase("The Series (BD)(640x480(RAW) (BATCH 1) (1-13)", false)]
[TestCase("[Doki] Series - 02 (848x480 XviD BD MP3) [95360783]", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01.BluRay.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.480i.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray480p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray480p, proper);
@@ -309,6 +311,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Sans.Series.De.Traces.FRENCH.720p.BluRay.x264-FHD", false)]
[TestCase("Series.Black.1x01.Selezione.Naturale.ITA.720p.BDMux.x264-NovaRip", false)]
[TestCase("Series.Hunter.S02.720p.Blu-ray.Remux.AVC.FLAC.2.0-SiCFoI", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.720p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray720p, proper);
@@ -340,6 +343,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S03E01.The.Calm.1080p.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR", false)]
[TestCase("Series Title Season 2 (BDRemux 1080p HEVC FLAC) [Netaro]", false)]
[TestCase("[Vodes] Series Title - Other Title (2020) [BDRemux 1080p HEVC Dual-Audio]", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.1080p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray1080p_remux_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray1080pRemux, proper);
@@ -360,6 +364,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Title.S01E08.The.Sonarr.BluRay.2160p.AVC.DTS-HD.MA.5.1.REMUX-FraMeSToR", false)]
[TestCase("Series.Title.2x11.Nato.Per.The.Sonarr.Bluray.Remux.AVC.2160p.AC3.ITA", false)]
[TestCase("[Dolby Vision] Sonarr.of.Series.S07.MULTi.UHD.BLURAY.REMUX.DV-NoTag", false)]
[TestCase("Adventures.of.Sonic.the.Hedgehog.S01E01.Best.Hedgehog.2160p.DD.2.0.AVC.REMUX-FraMeSToR", false)]
public void should_parse_bluray2160p_remux_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.Bluray2160pRemux, proper);

View File

@@ -75,6 +75,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("The.Series.2016.S02.Part.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series 2016", 2, 1)]
[TestCase("The.Series.S07.Vol.1.1080p.NF.WEBRip.DD5.1.x264-NTb", "The Series", 7, 1)]
[TestCase("The.Series.S06.P1.1080p.Blu-Ray.10-Bit.Dual-Audio.TrueHD.x265-iAHD", "The Series", 6, 1)]
public void should_parse_partial_season_release(string postTitle, string title, int season, int seasonPart)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -40,11 +40,29 @@ namespace NzbDrone.Core.Blocklisting
Delete(x => seriesIds.Contains(x.SeriesId));
}
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType).Join<Blocklist, Series>((b, m) => b.SeriesId == m.Id);
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder sql) => _database.QueryJoined<Blocklist, Series>(sql, (bl, movie) =>
{
bl.Series = movie;
return bl;
});
public override PagingSpec<Blocklist> GetPaged(PagingSpec<Blocklist> pagingSpec)
{
pagingSpec.Records = GetPagedRecords(PagedBuilder(), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(Blocklist))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder().Select(typeof(Blocklist)), pagingSpec, countTemplate);
return pagingSpec;
}
protected override SqlBuilder PagedBuilder()
{
var builder = Builder()
.Join<Blocklist, Series>((b, m) => b.SeriesId == m.Id);
return builder;
}
protected override IEnumerable<Blocklist> PagedQuery(SqlBuilder builder) =>
_database.QueryJoined<Blocklist, Series>(builder, (blocklist, series) =>
{
blocklist.Series = series;
return blocklist;
});
}
}

View File

@@ -239,7 +239,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
// Avoid removing torrents that haven't reached the global max ratio.
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config);
item.CanMoveFiles = item.CanBeRemoved = torrent.State is "pausedUP" or "stoppedUP" && HasReachedSeedLimit(torrent, config);
switch (torrent.State)
{
@@ -248,7 +248,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Message = _localizationService.GetLocalizedString("DownloadClientQbittorrentTorrentStateError");
break;
case "pausedDL": // torrent is paused and has NOT finished downloading
case "stoppedDL": // torrent is stopped and has NOT finished downloading
case "pausedDL": // torrent is paused and has NOT finished downloading (qBittorrent < 5)
item.Status = DownloadItemStatus.Paused;
break;
@@ -259,7 +260,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
item.Status = DownloadItemStatus.Queued;
break;
case "pausedUP": // torrent is paused and has finished downloading:
case "pausedUP": // torrent is paused and has finished downloading (qBittorent < 5)
case "stoppedUP": // torrent is stopped and has finished downloading
case "uploading": // torrent is being seeded and data is being transferred
case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload

View File

@@ -13,8 +13,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
public bool IsFinished { get; set; }
public long Eta { get; set; }
public TransmissionTorrentStatus Status { get; set; }
public int SecondsDownloading { get; set; }
public int SecondsSeeding { get; set; }
public long SecondsDownloading { get; set; }
public long SecondsSeeding { get; set; }
public string ErrorString { get; set; }
public long DownloadedEver { get; set; }
public long UploadedEver { get; set; }

View File

@@ -183,7 +183,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
if (series.Tags.Any())
{
var tags = _tagRepo.Get(series.Tags);
var tags = _tagRepo.GetTags(series.Tags);
foreach (var tag in tags)
{

View File

@@ -48,7 +48,7 @@ namespace NzbDrone.Core.Indexers
public class SeedCriteriaSettings
{
[FieldDefinition(0, Type = FieldType.Textbox, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
[FieldDefinition(0, Type = FieldType.Number, Label = "IndexerSettingsSeedRatio", HelpText = "IndexerSettingsSeedRatioHelpText")]
public double? SeedRatio { get; set; }
[FieldDefinition(1, Type = FieldType.Number, Label = "IndexerSettingsSeedTime", Unit = "minutes", HelpText = "IndexerSettingsSeedTimeHelpText", Advanced = true)]

View File

@@ -1,6 +1,6 @@
{
"Added": "Afegit",
"ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau de l'API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració",
"ApiKeyValidationHealthCheckMessage": "Actualitzeu la vostra clau API perquè tingui almenys {length} caràcters. Podeu fer-ho mitjançant la configuració o el fitxer de configuració",
"AgeWhenGrabbed": "Edat (quan es captura)",
"AuthenticationRequired": "Autenticació necessària",
"AutomaticAdd": "Afegeix automàticament",
@@ -49,7 +49,7 @@
"Default": "Per defecte",
"DeleteImportList": "Suprimeix la llista d'importació",
"DeleteSelectedImportLists": "Suprimeix la(es) llista(es) d'importació",
"DeletedReasonManual": "El fitxer va ser suprimit mitjançant la interfície d'usuari",
"DeletedReasonManual": "El fitxer s'ha suprimit mitjançant {appName}, ja sigui manualment o amb una altra eina mitjançant l'API",
"AbsoluteEpisodeNumbers": "Números d'episodis absoluts",
"AirDate": "Data d'emissió",
"DefaultNameCopiedProfile": "{name} - Còpia",
@@ -225,8 +225,8 @@
"BlocklistReleases": "Llista de llançaments bloquejats",
"BuiltIn": "Integrat",
"BrowserReloadRequired": "Es requereix una recàrrega del navegador",
"ApplicationUrlHelpText": "URL extern d'aquesta aplicació, inclòs http(s)://, port i URL base",
"AuthBasic": "Basic (finestra emergent del navegador)",
"ApplicationUrlHelpText": "URL extern de l'aplicació, inclòs http(s)://, port i URL base",
"AuthBasic": "Bàsic (finestra emergent del navegador)",
"AutomaticSearch": "Cerca automàtica",
"Automatic": "Automàtic",
"BindAddress": "Adreça d'enllaç",
@@ -239,13 +239,13 @@
"BranchUpdate": "Branca que s'utilitza per a actualitzar {appName}",
"CalendarLoadError": "No es pot carregar el calendari",
"AudioInfo": "Informació d'àudio",
"ApplyTagsHelpTextRemove": "Eliminar: elimina les etiquetes introduïdes",
"ApplyTagsHelpTextRemove": "Eliminació: elimina les etiquetes introduïdes",
"AutoAdd": "Afegeix automàticament",
"Apply": "Aplica",
"Backup": "Còpia de seguretat",
"Blocklist": "Llista de bloquejats",
"Calendar": "Calendari",
"ApplyTagsHelpTextAdd": "Afegeix: afegeix les etiquetes a la llista d'etiquetes existent",
"ApplyTagsHelpTextAdd": "Afegiment: afegeix les etiquetes a la llista d'etiquetes existent",
"AptUpdater": "Utilitzeu apt per a instal·lar l'actualització",
"BackupNow": "Fes ara la còpia de seguretat",
"AppDataDirectory": "Directori AppData",
@@ -411,7 +411,7 @@
"PrioritySettings": "Prioritat: {priority}",
"QualitiesLoadError": "No es poden carregar perfils de qualitat",
"RemoveSelectedItemQueueMessageText": "Esteu segur que voleu eliminar 1 element de la cua?",
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau de l'API?",
"ResetAPIKeyMessageText": "Esteu segur que voleu restablir la clau API?",
"TheLogLevelDefault": "El nivell de registre per defecte és \"Info\" i es pot canviar a [Configuració general](/configuració/general)",
"ClickToChangeLanguage": "Feu clic per a canviar l'idioma",
"ClickToChangeEpisode": "Feu clic per a canviar l'episodi",
@@ -669,7 +669,7 @@
"AutoTaggingSpecificationOriginalLanguage": "Llenguatge",
"AutoTaggingSpecificationQualityProfile": "Perfil de Qualitat",
"AutoTaggingSpecificationRootFolder": "Carpeta arrel",
"AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar",
"AddDelayProfileError": "No s'ha pogut afegir un perfil de retard, torna-ho a provar.",
"AutoTaggingSpecificationSeriesType": "Tipus de Sèries",
"AutoTaggingSpecificationStatus": "Estat",
"BlocklistAndSearch": "Llista de bloqueig i cerca",
@@ -711,5 +711,37 @@
"MinutesSixty": "60 minuts: {sixty}",
"CustomFilter": "Filtres personalitzats",
"CustomFormatsSpecificationRegularExpressionHelpText": "El format personalitzat RegEx no distingeix entre majúscules i minúscules",
"CustomFormatsSpecificationFlag": "Bandera"
"CustomFormatsSpecificationFlag": "Bandera",
"DeleteSelectedSeries": "Suprimeix les sèries seleccionades",
"DeleteSeriesFolder": "Suprimeix la carpeta de sèries",
"DeleteSeriesFolderConfirmation": "La carpeta de sèries '{path}' i tot el seu contingut es suprimiran.",
"File": "Fitxer",
"FilterContains": "conté",
"FilterIs": "és",
"Ok": "D'acord",
"Qualities": "Qualitats",
"DeleteSeriesFolderCountConfirmation": "Esteu segur que voleu suprimir {count} sèries seleccionades?",
"Forecast": "Previsió",
"Paused": "En pausa",
"Range": "Rang",
"Today": "Avui",
"Month": "Mes",
"Reason": "Raó",
"Pending": "Pendents",
"FilterEqual": "igual",
"Global": "Global",
"Grab": "Captura",
"Logout": "Tanca la sessió",
"Filter": "Filtre",
"Local": "Local",
"Week": "Setmana",
"AutoTaggingSpecificationTag": "Etiqueta",
"WhatsNew": "Novetats",
"DownloadClientSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {clientName}, com ara {url}",
"IndexerHDBitsSettingsCodecs": "Còdecs",
"DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de rTorrent",
"IndexerHDBitsSettingsMediums": "Mitjans",
"UpdateStartupTranslocationHealthCheckMessage": "No es pot instal·lar l'actualització perquè la carpeta d'inici '{startupFolder}' es troba en una carpeta de translocació d'aplicacions.",
"UpdateStartupNotWritableHealthCheckMessage": "No es pot instal·lar l'actualització perquè l'usuari '{userName}' no té permisos d'escriptura de la carpeta d'inici '{startupFolder}'.",
"DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Transmission"
}

View File

@@ -158,6 +158,7 @@
"BlocklistAndSearch": "Blocklist and Search",
"BlocklistAndSearchHint": "Start a search for a replacement after blocklisting",
"BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting",
"BlocklistFilterHasNoItems": "Selected blocklist filter contains no items",
"BlocklistLoadError": "Unable to load blocklist",
"BlocklistMultipleOnlyHint": "Blocklist without searching for replacements",
"BlocklistOnly": "Blocklist Only",
@@ -248,8 +249,8 @@
"ConnectionLost": "Connection Lost",
"ConnectionLostReconnect": "{appName} will try to connect automatically, or you can click reload below.",
"ConnectionLostToBackend": "{appName} has lost its connection to the backend and will need to be reloaded to restore functionality.",
"Connections": "Connections",
"ConnectionSettingsUrlBaseHelpText": "Adds a prefix to the {connectionName} url, such as {url}",
"Connections": "Connections",
"Continuing": "Continuing",
"ContinuingOnly": "Continuing Only",
"ContinuingSeriesDescription": "More episodes/another season is expected",
@@ -280,8 +281,8 @@
"CustomFormats": "Custom Formats",
"CustomFormatsLoadError": "Unable to load Custom Formats",
"CustomFormatsSettings": "Custom Formats Settings",
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
"CustomFormatsSpecificationFlag": "Flag",
"CustomFormatsSpecificationLanguage": "Language",
"CustomFormatsSpecificationMaximumSize": "Maximum Size",
@@ -410,16 +411,16 @@
"DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location",
"DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {downloadClientName}. {errorMessage}",
"DownloadClientDelugeSettingsDirectory": "Download Directory",
"DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory",
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location",
"DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}",
"DownloadClientDelugeTorrentStateError": "Deluge is reporting an error",
"DownloadClientDelugeValidationLabelPluginFailure": "Configuration of label failed",
"DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} was unable to add the label to {clientName}.",
"DownloadClientDelugeValidationLabelPluginInactive": "Label plugin not activated",
"DownloadClientDelugeValidationLabelPluginInactiveDetail": "You must have the Label plugin enabled in {clientName} to use categories.",
"DownloadClientDelugeSettingsDirectory": "Download Directory",
"DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location",
"DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory",
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location",
"DownloadClientDownloadStationProviderMessage": "{appName} is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account",
"DownloadClientDownloadStationSettingsDirectoryHelpText": "Optional shared folder to put downloads into, leave blank to use the default Download Station location",
"DownloadClientDownloadStationValidationApiVersion": "Download Station API version not supported, should be at least {requiredVersion}. It supports from {minVersion} to {maxVersion}",
@@ -751,6 +752,7 @@
"Group": "Group",
"HardlinkCopyFiles": "Hardlink/Copy Files",
"HasMissingSeason": "Has Missing Season",
"HasUnmonitoredSeason": "Has Unmonitored Season",
"Health": "Health",
"HealthMessagesInfoBox": "You can find more information about the cause of these health check messages by clicking the wiki link (book icon) at the end of the row, or by checking your [logs]({link}). If you have difficulty interpreting these messages then you can reach out to our support, at the links below.",
"Here": "here",
@@ -867,12 +869,12 @@
"ImportListsSimklSettingsUserListTypePlanToWatch": "Plan To Watch",
"ImportListsSimklSettingsUserListTypeWatching": "Watching",
"ImportListsSonarrSettingsApiKeyHelpText": "API Key of the {appName} instance to import from",
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring",
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored",
"ImportListsSonarrSettingsFullUrl": "Full URL",
"ImportListsSonarrSettingsFullUrlHelpText": "URL, including port, of the {appName} instance to import from",
"ImportListsSonarrSettingsQualityProfilesHelpText": "Quality Profiles from the source instance to import from",
"ImportListsSonarrSettingsRootFoldersHelpText": "Root Folders from the source instance to import from",
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring",
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored",
"ImportListsSonarrSettingsTagsHelpText": "Tags from the source instance to import from",
"ImportListsSonarrValidationInvalidUrl": "{appName} URL is invalid, are you missing a URL base?",
"ImportListsTraktSettingsAdditionalParameters": "Additional Parameters",
@@ -976,11 +978,11 @@
"IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.",
"IndexerSettingsMinimumSeeders": "Minimum Seeders",
"IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.",
"IndexerSettingsMultiLanguageRelease": "Multi Languages",
"IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?",
"IndexerSettingsPasskey": "Passkey",
"IndexerSettingsRejectBlocklistedTorrentHashes": "Reject Blocklisted Torrent Hashes While Grabbing",
"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.",
"IndexerSettingsMultiLanguageRelease": "Multi Languages",
"IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?",
"IndexerSettingsRssUrl": "RSS URL",
"IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed",
"IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time",
@@ -1790,7 +1792,6 @@
"SelectSeries": "Select Series",
"SendAnonymousUsageData": "Send Anonymous Usage Data",
"Series": "Series",
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them]({url}) .",
"SeriesCannotBeFound": "Sorry, that series cannot be found.",
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} episode files",
@@ -1804,6 +1805,7 @@
"SeriesFolderFormat": "Series Folder Format",
"SeriesFolderFormatHelpText": "Used when adding a new series or moving series via the series editor",
"SeriesFolderImportedTooltip": "Episode imported from series folder",
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
"SeriesID": "Series ID",
"SeriesIndexFooterContinuing": "Continuing (All episodes downloaded)",
"SeriesIndexFooterDownloading": "Downloading (One or more episodes)",

View File

@@ -249,7 +249,7 @@
"ConnectionLostToBackend": "{appName} ha perdido su conexión con el backend y tendrá que ser recargado para restaurar su funcionalidad.",
"CalendarOptions": "Opciones de Calendario",
"BypassDelayIfAboveCustomFormatScoreHelpText": "Habilitar ignorar cuando la versión tenga una puntuación superior a la puntuación mínima configurada para el formato personalizado",
"Default": "Por defecto",
"Default": "Predeterminado",
"DeleteBackupMessageText": "Seguro que quieres eliminar la copia de seguridad '{name}'?",
"DeleteAutoTagHelpText": "¿Está seguro de querer eliminar el etiquetado automático '{name}'?",
"AddImportListImplementation": "Añadir lista de importación - {implementationName}",
@@ -1964,7 +1964,7 @@
"NotificationsValidationInvalidAuthenticationToken": "Token de autenticación inválido",
"NotificationsValidationUnableToConnect": "No se pudo conectar: {exceptionMessage}",
"NotificationsValidationUnableToSendTestMessageApiResponse": "No se pudo enviar un mensaje de prueba. Respuesta de la API: {error}",
"OverrideGrabModalTitle": "Sobrescribe y captura - {title}",
"OverrideGrabModalTitle": "Sobrescribir y capturar - {title}",
"ReleaseProfileTagSeriesHelpText": "Los perfiles de lanzamientos se aplicarán a series con al menos una etiqueta coincidente. Deja en blanco para aplicar a todas las series",
"ReleaseSceneIndicatorMappedNotRequested": "El episodio mapeado no fue solicitado en esta búsqueda.",
"RemotePathMappingBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.",
@@ -2071,5 +2071,8 @@
"NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente prefija el título del mensaje con {appName} para diferenciar las notificaciones de las diferentes aplicaciones",
"IndexerSettingsMultiLanguageRelease": "Múltiples idiomas",
"IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?"
"IndexerSettingsMultiLanguageReleaseHelpText": "¿Qué idiomas están normalmente en un lanzamiento múltiple en este indexador?",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está reportando archivos faltantes",
"BlocklistFilterHasNoItems": "El filtro de lista de bloqueo seleccionado no contiene ningún elemento",
"HasUnmonitoredSeason": "Tiene temporada sin monitorizar"
}

View File

@@ -1190,7 +1190,7 @@
"FilterNotInNext": "não no próximo",
"FilterSeriesPlaceholder": "Filtrar séries",
"FilterStartsWith": "começa com",
"GrabRelease": "Obter lançamento",
"GrabRelease": "Baixar Lançamento",
"HardlinkCopyFiles": "Hardlink/Copiar Arquivos",
"InteractiveImportNoEpisode": "Escolha um ou mais episódios para cada arquivo selecionado",
"InteractiveImportNoImportMode": "Defina um modo de importação",
@@ -1203,7 +1203,7 @@
"KeyboardShortcutsOpenModal": "Abrir este pop-up",
"Local": "Local",
"Logout": "Sair",
"ManualGrab": "Obter manualmente",
"ManualGrab": "Baixar Manualmente",
"ManualImport": "Importação manual",
"Mapping": "Mapeamento",
"MarkAsFailedConfirmation": "Tem certeza de que deseja marcar \"{sourceTitle}\" como em falha?",
@@ -1229,7 +1229,7 @@
"RemoveFilter": "Remover filtro",
"RootFolderSelectFreeSpace": "{freeSpace} Livre",
"Search": "Pesquisar",
"SelectDownloadClientModalTitle": "{modalTitle} - Selecione o Cliente de Download",
"SelectDownloadClientModalTitle": "{modalTitle} - Selecionar Cliente de Download",
"SelectReleaseGroup": "Selecionar um Grupo de Lançamento",
"SelectSeason": "Selecionar Temporada",
"SelectSeasonModalTitle": "{modalTitle} - Selecione a Temporada",
@@ -2071,5 +2071,7 @@
"SeriesFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Series Title:30}`) ou do início (por exemplo, `{Series Title:-30}`) é suportado.",
"IndexerSettingsMultiLanguageReleaseHelpText": "Quais idiomas normalmente estão em um lançamento multi neste indexador?",
"AutoTaggingSpecificationTag": "Etiqueta",
"IndexerSettingsMultiLanguageRelease": "Multi Idiomas"
"IndexerSettingsMultiLanguageRelease": "Multi Idiomas",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent está relatando arquivos perdidos",
"BlocklistFilterHasNoItems": "O filtro da lista de bloqueio selecionado não contém itens"
}

View File

@@ -31,7 +31,7 @@
"AddImportListExclusion": "İçe Aktarma Listesi Hariç Tutma Ekle",
"AddImportListExclusionError": "Yeni bir içe aktarım listesi dışlaması eklenemiyor, lütfen tekrar deneyin.",
"AddImportListImplementation": "İçe Aktarım Listesi Ekle -{implementationName}",
"AddIndexer": "Dizin Oluşturucu Ekle",
"AddIndexer": "Dizinleyici Ekle",
"AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın",
"AddNotificationError": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.",
"AddReleaseProfile": "Yayın Profili Ekle",
@@ -40,7 +40,7 @@
"AddSeriesWithTitle": "{title} Ekleyin",
"Agenda": "Ajanda",
"Airs": "Yayınlar",
"AddIndexerError": "Yeni dizin oluşturucu eklenemiyor, lütfen tekrar deneyin.",
"AddIndexerError": "Yeni dizinleyici eklenemiyor, lütfen tekrar deneyin.",
"AddNewSeriesSearchForCutoffUnmetEpisodes": "Karşılanmamış bölümleri aramaya başlayın",
"AddQualityProfileError": "Yeni kalite profili eklenemiyor, lütfen tekrar deneyin.",
"AddRemotePathMappingError": "Yeni bir uzak yol eşlemesi eklenemiyor, lütfen tekrar deneyin.",
@@ -68,7 +68,7 @@
"AddRootFolderError": "Kök klasör eklenemiyor",
"CountImportListsSelected": "{count} içe aktarma listesi seçildi",
"CustomFormatsSpecificationFlag": "Bayrak",
"ClickToChangeIndexerFlags": "Dizin oluşturucu bayraklarını değiştirmek için tıklayın",
"ClickToChangeIndexerFlags": "Dizinleyici bayraklarını değiştirmek için tıklayın",
"ClickToChangeReleaseGroup": "Yayım grubunu değiştirmek için tıklayın",
"AppUpdated": "{appName} Güncellendi",
"ApplicationURL": "Uygulama URL'si",
@@ -127,7 +127,7 @@
"Category": "Kategori",
"CertificateValidationHelpText": "HTTPS sertifika doğrulamasının sıkılığını değiştirin. Riskleri anlamadığınız sürece değişmeyin.",
"CloneCondition": "Klon Durumu",
"CountIndexersSelected": "{count} dizin oluşturucu seçildi",
"CountIndexersSelected": "{count} dizinleyici seçildi",
"CustomFormatsSpecificationRegularExpressionHelpText": "Özel Format RegEx Büyük/Küçük Harfe Duyarsızdır",
"AutoRedownloadFailed": "Yeniden İndirme Başarısız",
"AutoRedownloadFailedFromInteractiveSearch": "Etkileşimli Aramadan Yeniden İndirme Başarısız Oldu",
@@ -155,7 +155,7 @@
"DelayMinutes": "{delay} Dakika",
"DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?",
"DeleteReleaseProfile": "Yayımlama Profilini Sil",
"DeleteSelectedIndexers": "Dizin Oluşturucuları Sil",
"DeleteSelectedIndexers": "Dizinleyicileri Sil",
"Directory": "Rehber",
"Donate": "Bağış yap",
"DownloadClientDownloadStationValidationFolderMissing": "Klasör mevcut değil",
@@ -176,7 +176,7 @@
"DeleteConditionMessageText": "'{name}' koşulunu silmek istediğinizden emin misiniz?",
"DeleteImportListExclusionMessageText": "Bu içe aktarma listesi hariç tutma işlemini silmek istediğinizden emin misiniz?",
"DeleteQualityProfileMessageText": "'{name}' kalite profilini silmek istediğinizden emin misiniz?",
"DeleteSelectedIndexersMessageText": "Seçilen {count} dizin oluşturucuyu silmek istediğinizden emin misiniz?",
"DeleteSelectedIndexersMessageText": "Seçilen {count} dizinleyiciyi silmek istediğinizden emin misiniz?",
"DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName}, etiketi {clientName} uygulamasına ekleyemedi.",
"DownloadClientDownloadStationProviderMessage": "DSM hesabınızda 2 Faktörlü Kimlik Doğrulama etkinleştirilmişse {appName}, Download Station'a bağlanamaz",
"DownloadClientDownloadStationValidationApiVersion": "Download Station API sürümü desteklenmiyor; en az {requiredVersion} olmalıdır. {minVersion}'dan {maxVersion}'a kadar destekler",
@@ -195,13 +195,13 @@
"DownloadClientDelugeValidationLabelPluginFailure": "Etiket yapılandırılması başarısız oldu",
"DownloadClientDownloadStationValidationSharedFolderMissing": "Paylaşılan klasör mevcut değil",
"DeleteImportList": "İçe Aktarma Listesini Sil",
"IndexerPriorityHelpText": "Dizin Oluşturucu Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek",
"IndexerPriorityHelpText": "Dizinleyici Önceliği (En Yüksek) 1'den (En Düşük) 50'ye kadar. Varsayılan: 25'dir. Eşit olmayan yayınlar için eşitlik bozucu olarak yayınlar alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek",
"DisabledForLocalAddresses": "Yerel Adresler için Devre Dışı Bırakıldı",
"DownloadClientDelugeValidationLabelPluginInactive": "Etiket eklentisi etkinleştirilmedi",
"DownloadClientDelugeValidationLabelPluginInactiveDetail": "Kategorileri kullanmak için {clientName} uygulamasında Etiket eklentisini etkinleştirmiş olmanız gerekir.",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Diskstation'ınızda {username} olarak oturum açmalı ve BT/HTTP/FTP/NZB -> Konum altında DownloadStation ayarlarında manuel olarak ayarlamalısınız.",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation'da '{sharedFolder}' adında bir Paylaşımlı Klasör yok, bunu doğru belirttiğinizden emin misiniz?",
"DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizin Oluşturucular'daki mevcut tohum kriterlerine göre torrentlerin otomatik olarak kaldırılmasını gerçekleştirecek",
"DownloadClientFloodSettingsRemovalInfo": "{appName}, Ayarlar -> Dizinleyiciler'deki geçerli başlangıç ölçütlerine göre torrentlerin otomatik olarak kaldırılmasını sağlar",
"Database": "Veri tabanı",
"DelayProfileProtocol": "Protokol: {preferredProtocol}",
"DownloadClientDownloadStationValidationFolderMissingDetail": "'{downloadDir}' klasörü mevcut değil, '{sharedFolder}' Paylaşımlı Klasöründe manuel olarak oluşturulması gerekiyor.",
@@ -390,9 +390,9 @@
"FailedToFetchUpdates": "Güncellemeler getirilemedi",
"InstanceName": "Örnek isim",
"MoveAutomatically": "Otomatik Olarak Taşı",
"MustContainHelpText": "İzin bu şartlardan en az birini içermelidir (büyük/küçük harfe duyarlı değildir)",
"MustContainHelpText": "Yayın, bu terimlerden en az birini içermelidir (büyük / küçük harfe duyarsız)",
"NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor",
"EditSelectedIndexers": "Seçili Dizin Oluşturucuları Düzenle",
"EditSelectedIndexers": "Seçili Dizinleyicileri Düzenle",
"EnableProfileHelpText": "Yayımlama profilini etkinleştirmek için işaretleyin",
"EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli periyotlarda yayın değişikliği aradığında kullanacak",
"FormatTimeSpanDays": "{days}g {time}",
@@ -407,13 +407,13 @@
"FormatRuntimeHours": "{hours}s",
"LanguagesLoadError": "Diller yüklenemiyor",
"ListWillRefreshEveryInterval": "Liste her {refreshInterval} yenilenecektir",
"ManageIndexers": "Dizin Oluşturucuları Yönet",
"ManageIndexers": "Dizinleyicileri Yönet",
"ManualGrab": "Manuel Yakalama",
"DownloadClientsSettingsSummary": "İndirme İstemcileri, indirme işlemleri ve uzaktan yol eşlemeleri",
"DownloadClients": "İndirme İstemcileri",
"InteractiveImportNoFilesFound": "Seçilen klasörde video dosyası bulunamadı",
"ListQualityProfileHelpText": "Kalite Profili listesi öğeleri şu şekilde eklenecektir:",
"MustNotContainHelpText": "Bir veya daha fazla terimi içeriyorsa yayın reddedilecektir (büyük/küçük harfe duyarlı değildir)",
"MustNotContainHelpText": "Yayın, bir veya daha fazla terim içeriyorsa reddedilir (büyük/ küçük harfe duyarsız)",
"NoDownloadClientsFound": "İndirme istemcisi bulunamadı",
"NotificationStatusSingleClientHealthCheckMessage": "Arızalar nedeniyle bildirimler kullanılamıyor: {notificationNames}",
"NotificationsAppriseSettingsStatelessUrls": "Apprise Durum bilgisi olmayan URL'ler",
@@ -446,7 +446,7 @@
"MediaInfoFootNote": "Full/AudioLanguages/SubtitleLanguages, dosya adında yer alan dilleri filtrelemenize olanak tanıyan bir `:EN+DE` son ekini destekler. Belirli dilleri hariç tutmak için '-DE'yi kullanın. `+` (örneğin `:EN+`) eklenmesi, hariç tutulan dillere bağlı olarak `[EN]`/`[EN+--]`/`[--]` sonucunu verecektir. Örneğin `{MediaInfo Full:EN+DE}`.",
"Never": "Asla",
"NoHistoryBlocklist": "Geçmiş engellenenler listesi yok",
"NoIndexersFound": "Dizin oluşturucu bulunamadı",
"NoIndexersFound": "Dizinleyici bulunamadı",
"NotificationsAppriseSettingsConfigurationKeyHelpText": "Kalıcı Depolama Çözümü için Yapılandırma Anahtarı. Durum Bilgisi Olmayan URL'ler kullanılıyorsa boş bırakın.",
"NotificationsAppriseSettingsPasswordHelpText": "HTTP Temel Kimlik Doğrulama Parolası",
"NotificationsAppriseSettingsUsernameHelpText": "HTTP Temel Kimlik Doğrulama Kullanıcı Adı",
@@ -488,7 +488,7 @@
"Test": "Sına",
"HealthMessagesInfoBox": "Satırın sonundaki wiki bağlantısını (kitap simgesi) tıklayarak veya [günlüklerinizi]({link}) kontrol ederek bu durum kontrolü mesajlarının nedeni hakkında daha fazla bilgi bulabilirsiniz. Bu mesajları yorumlamakta zorluk yaşıyorsanız aşağıdaki bağlantılardan destek ekibimize ulaşabilirsiniz.",
"IndexerSettingsRejectBlocklistedTorrentHashes": "Yakalarken Engellenen Torrent Karmalarını Reddet",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent karma tarafından engellendiyse, RSS/Bazı dizin oluşturucuları arama sırasında düzgün şekilde reddedilmeyebilir; bunun etkinleştirilmesi, torrent yakalandıktan sonra ancak istemciye gönderilmeden önce reddedilmesine olanak tanır.",
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Bir torrent hash tarafından engellenirse, bazı dizinleyiciler için RSS / Arama sırasında düzgün bir şekilde reddedilmeyebilir, bunun etkinleştirilmesi, torrent yakalandıktan sonra, ancak istemciye gönderilmeden önce reddedilmesine izin verecektir.",
"NotificationsAppriseSettingsConfigurationKey": "Apprise Yapılandırma Anahtarı",
"NotificationsAppriseSettingsNotificationType": "Apprise Bildirim Türü",
"NotificationsGotifySettingsServerHelpText": "Gerekiyorsa http(s):// ve bağlantı noktası dahil olmak üzere Gotify sunucu URL'si",
@@ -506,7 +506,7 @@
"NotificationsKodiSettingsUpdateLibraryHelpText": "İçe Aktarma ve Yeniden Adlandırmada kitaplık güncellensin mi?",
"NotificationsNtfySettingsServerUrlHelpText": "Genel sunucuyu ({url}) kullanmak için boş bırakın",
"InteractiveImportLoadError": "Manuel içe aktarma öğeleri yüklenemiyor",
"IndexerDownloadClientHelpText": "Bu dizin oluşturucudan yakalamak için hangi indirme istemcisinin kullanılacağını belirtin",
"IndexerDownloadClientHelpText": "Bu dizinleyiciden yakalamak için hangi indirme istemcisinin kullanılacağını belirtin",
"DeleteRemotePathMapping": "Uzak Yol Eşlemeyi Sil",
"LastDuration": "Yürütme Süresi",
"NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName}, kitaplık yolu konumunu {appName}'den farklı gördüğünde seri yollarını değiştirmek için kullanılan {serviceName} yolu ('Kütüphaneyi Güncelle' gerektirir)",
@@ -557,9 +557,9 @@
"RemoveFailedDownloads": "Başarısız İndirmeleri Kaldır",
"Scheduled": "Planlı",
"Underscore": "Vurgula",
"SetIndexerFlags": "Dizin Oluşturucu Bayraklarını Ayarla",
"SetIndexerFlags": "Dizinleyici Bayraklarını Ayarla",
"SetReleaseGroup": "Yayımlama Grubunu Ayarla",
"SetIndexerFlagsModalTitle": "{modalTitle} - Dizin Oluşturucu Bayraklarını Ayarla",
"SetIndexerFlagsModalTitle": "{modalTitle} - Dizinleyici Bayraklarını Ayarla",
"SslCertPassword": "SSL Sertifika Şifresi",
"Rating": "Puan",
"GrabRelease": "Yayın Yakalama",
@@ -706,7 +706,7 @@
"PreviouslyInstalled": "Önceden Yüklenmiş",
"QualityCutoffNotMet": "Kalite sınırı karşılanmadı",
"QueueIsEmpty": "Kuyruk boş",
"ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizin oluşturucunun ayarlanması, bu profilin yalnızca söz konusu dizin oluşturucunun sürümlerine uygulanmasına neden olur.",
"ReleaseProfileIndexerHelpTextWarning": "Bir sürüm profilinde belirli bir dizinleyicinin ayarlanması, bu profilin yalnızca söz konusu dizinleyicinin yayınlarına uygulanmasına neden olur.",
"ResetDefinitionTitlesHelpText": "Değerlerin yanı sıra tanım başlıklarını da sıfırlayın",
"SecretToken": "Gizlilik Jetonu",
"SetReleaseGroupModalTitle": "{modalTitle} - Yayımlama Grubunu Ayarla",
@@ -722,7 +722,7 @@
"Uptime": "Çalışma süresi",
"RemotePath": "Uzak Yol",
"File": "Dosya",
"ReleaseProfileIndexerHelpText": "Profilin hangi dizin oluşturucuya uygulanacağını belirtin",
"ReleaseProfileIndexerHelpText": "Profilin hangi dizinleyiciye uygulanacağını belirtin",
"TablePageSize": "Sayfa Boyutu",
"NotificationsSynologyValidationTestFailed": "Synology veya synoındex mevcut değil",
"NotificationsTwitterSettingsAccessTokenSecret": "Erişim Jetonu Gizliliği",
@@ -738,7 +738,7 @@
"RemoveQueueItemRemovalMethod": "Kaldırma Yöntemi",
"RemoveQueueItemRemovalMethodHelpTextWarning": "'İndirme İstemcisinden Kaldır', indirme işlemini ve dosyaları indirme istemcisinden kaldıracaktır.",
"RemoveSelectedItems": "Seçili öğeleri kaldır",
"SelectIndexerFlags": "Dizin Oluşturucu Bayraklarını Seçin",
"SelectIndexerFlags": "Dizinleyici Bayraklarını Seçin",
"Started": "Başlatıldı",
"Size": "Boyut",
"SupportedCustomConditions": "{appName}, aşağıdaki yayın özelliklerine göre özel koşulları destekler.",
@@ -792,5 +792,15 @@
"Logging": "Loglama",
"MinutesSixty": "60 Dakika: {sixty}",
"SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin",
"Repack": "Yeniden paketle"
"Repack": "Yeniden paketle",
"IndexerFlags": "Dizinleyici Bayrakları",
"Indexer": "Dizinleyici",
"Indexers": "Dizinleyiciler",
"IndexerPriority": "Dizinleyici Önceliği",
"IndexerOptionsLoadError": "Dizinleyici seçenekleri yüklenemiyor",
"IndexerSettings": "Dizinleyici Ayarları",
"MustContain": "İçermeli",
"MustNotContain": "İçermemeli",
"RssSyncIntervalHelpTextWarning": "Bu, tüm dizinleyiciler için geçerli olacaktır, lütfen onlar tarafından belirlenen kurallara uyun",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent eksik dosya raporluyor"
}

View File

@@ -104,5 +104,246 @@
"AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.",
"AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм",
"AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль",
"AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача"
"AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача",
"DeleteSelectedEpisodeFiles": "Видалити вибрані файли серій",
"DownloadClientQbittorrentTorrentStateUnknown": "Невідомий стан завантаження: {state}",
"DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} не намагатиметься імпортувати завершені завантаження без категорії.",
"AddedDate": "Додано: {date}",
"AnEpisodeIsDownloading": "Виконується завантаження серії",
"Blocklist": "Чорний список",
"CalendarFeed": "Стрічка календаря {appName}",
"CancelProcessing": "Скасувати обробку",
"ChmodFolderHelpTextWarning": "Це працює лише в тому випадку, якщо власником файлу є користувач, на якому працює {appName}. Краще переконатися, що клієнт завантаження правильно встановлює дозволи.",
"ChownGroupHelpTextWarning": "Це працює, лише якщо користувач, який запускає {appName}, є власником файлу. Краще переконатися, що клієнт завантаження використовує ту саму групу, що й {appName}.",
"ClickToChangeSeries": "Натисніть, щоб змінити серіал",
"ConnectSettings": "Налаштування підключення",
"ConnectionLostReconnect": "{appName} спробує підключитися автоматично, або ви можете натиснути «Перезавантажити» нижче.",
"ContinuingOnly": "Тільки продовження",
"CustomFormatJson": "Спеціальний формат JSON",
"DeleteSelectedEpisodeFilesHelpText": "Ви впевнені, що бажаєте видалити вибрані файли серії?",
"DeleteSeriesFolderCountWithFilesConfirmation": "Ви впевнені, що хочете видалити {count} вибраних серіалів і весь вміст?",
"DeleteSeriesFolderCountConfirmation": "Ви впевнені, що хочете видалити {count} вибраних серіалів?",
"DeletedSeriesDescription": "Серіал видалено з TheTVDB",
"DeleteSpecificationHelpText": "Ви впевнені, що хочете видалити специфікацію '{name}'?",
"DoNotPrefer": "Не віддавати перевагу",
"Disabled": "Вимкнено",
"Added": "Додано",
"AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Підтвердити новий пароль",
"CancelPendingTask": "Ви впевнені, що хочете скасувати це незавершене завдання?",
"Cancel": "Скасувати",
"ChownGroupHelpText": "Назва групи або gid. Використовуйте gid для віддалених файлових систем.",
"ClickToChangeQuality": "Натисніть, щоб змінити якість",
"ChooseImportMode": "Виберіть режим імпорту",
"Close": "Закрити",
"ClickToChangeSeason": "Натисніть, щоб змінити сезон",
"ConditionUsingRegularExpressions": "Ця умова відповідає використанню регулярних виразів. Зауважте, що символи `\\^$.|?*+()[{` мають спеціальні значення та потребують екранування за допомогою `\\`",
"ConnectionLostToBackend": "{appName} втратив з’єднання з серверною частиною, і його потрібно перезавантажити, щоб відновити роботу.",
"ConnectionLost": "Зв'язок втрачений",
"ContinuingSeriesDescription": "Більше серій/очікується ще один сезон",
"CreateGroup": "Створити групу",
"CustomFormatUnknownConditionOption": "Невідомий параметр '{key}' для умови '{implementation}'",
"Default": "За замовчуванням",
"DelayProfile": "Профіль затримки",
"DelayMinutes": "{delay} Хвилин",
"DeleteEpisodeFile": "Видалити файл серії",
"DeleteEpisodeFileMessage": "Ви впевнені, що хочете видалити '{path}'?",
"DeleteIndexer": "Видалити індексатор",
"DeleteIndexerMessageText": "Ви впевнені, що хочете видалити індексатор \"{name}\"?",
"DeleteSelectedSeries": "Видалити вибраний серіал",
"DeleteSeriesFolderHelpText": "Видалити папку серіалу та її вміст",
"DeleteTag": "Видалити тег",
"DeleteSeriesModalHeader": "Видалити - {title}",
"DeleteSpecification": "Видалити специфікацію",
"DeletedReasonManual": "Файл було видалено за допомогою {appName} вручну або іншим інструментом через API",
"DestinationPath": "Шлях призначення",
"DetailedProgressBar": "Детальний індикатор прогресу",
"DetailedProgressBarHelpText": "Показати текст на панелі виконання",
"DisabledForLocalAddresses": "Вимкнено для локальних адрес",
"Discord": "Discord",
"DoNotUpgradeAutomatically": "Не оновлювати автоматично",
"DiskSpace": "Дисковий простір",
"DoneEditingGroups": "Редагування груп завершено",
"DownloadClientQbittorrentValidationCategoryUnsupported": "Категорія не підтримується",
"DeleteImportList": "Видалити список імпорту",
"DeleteReleaseProfile": "Видалити профіль випуску",
"DeleteCustomFormatMessageText": "Ви впевнені, що хочете видалити спеціальний формат \"{name}\"?",
"DeleteRootFolder": "Видалити кореневу папку",
"DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Категорії не підтримуються до qBittorrent версії 3.3.0. Оновіть або повторіть спробу з пустою категорією.",
"DownloadClientRootFolderHealthCheckMessage": "Клієнт завантаження {downloadClientName} розміщує завантаження в кореневій папці {rootFolderPath}. Ви не повинні завантажувати в кореневу папку.",
"CopyToClipboard": "Копіювати в буфер обміну",
"DeleteQualityProfile": "Видалити профіль якості",
"Calendar": "Календар",
"Daily": "Щодня",
"CustomFormatScore": "Оцінка спеціального формату",
"DotNetVersion": ".NET",
"Download": "Завантажити",
"DownloadClient": "Клієнт завантажувача",
"Component": "Компонент",
"AuthenticationMethodHelpText": "Вимагати ім’я користувача та пароль для доступу до {appName}",
"AuthenticationRequiredHelpText": "Змінити запити, для яких потрібна автентифікація. Не змінюйте, якщо не розумієте ризики.",
"BuiltIn": "Вбудований",
"ChangeFileDate": "Змінити дату файлу",
"ChooseAnotherFolder": "Виберіть іншу папку",
"CloneAutoTag": "Клонувати автоматичний тег",
"CollectionsLoadError": "Не вдалося завантажити колекції",
"CountSeasons": "{count} Сезонів",
"CustomFormatsLoadError": "Не вдалося завантажити спеціальні формати",
"DelayProfilesLoadError": "Неможливо завантажити профілі затримки",
"DelayProfileProtocol": "Протокол: {preferredProtocol}",
"DeleteAutoTag": "Видалити автоматичний тег",
"DeleteAutoTagHelpText": "Ви впевнені, що хочете видалити автоматичний тег \"{name}\"?",
"DeleteDelayProfile": "Видалити профіль затримки",
"DeleteDelayProfileMessageText": "Ви впевнені, що хочете видалити цей профіль затримки?",
"DeleteDownloadClientMessageText": "Ви впевнені, що хочете видалити клієнт завантаження '{name}'?",
"DeleteSelectedImportLists": "Видалити списки імпорту",
"Deleted": "Видалено",
"DeleteTagMessageText": "Ви впевнені, що хочете видалити тег '{label}'?",
"DestinationRelativePath": "Відносний шлях призначення",
"DownloadClientCheckNoneAvailableHealthCheckMessage": "Немає доступного клієнта для завантаження",
"Connections": "З'єднання",
"Docker": "Docker",
"AddingTag": "Додавання тега",
"AutomaticSearch": "Автоматичний пошук",
"Backup": "Резервне копіювання",
"Automatic": "Автоматично",
"BackupFolderHelpText": "Відносні шляхи будуть у каталозі AppData {appName}",
"CalendarLoadError": "Неможливо завантажити календар",
"ConnectionSettingsUrlBaseHelpText": "Додає префікс до URL-адреси {connectionName}, наприклад {url}",
"CustomFormatUnknownCondition": "Невідома умова спеціального формату '{implementation}'",
"DelayingDownloadUntil": "Завантаження відкладається до {date} о {time}",
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} не зміг додати мітку до qBittorrent.",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent повідомляє про помилку",
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent завантажує метадані",
"Connection": "Підключення",
"Continuing": "Продовження",
"Database": "База даних",
"AddSeriesWithTitle": "Додати {title}",
"AddToDownloadQueue": "Додати до черги завантажень",
"CalendarLegendEpisodeDownloadingTooltip": "Серія зараз завантажується",
"CalendarLegendEpisodeUnmonitoredTooltip": "Серія не відстежується",
"CalendarLegendEpisodeMissingTooltip": "Серія вийшла в ефір і відсутній на диску",
"CalendarLegendEpisodeUnairedTooltip": "Серія ще не вийшла в ефір",
"CalendarLegendSeriesFinaleTooltip": "Фінал серіалу або сезону",
"CalendarLegendSeriesPremiereTooltip": "Прем'єра серіалу або сезону",
"Category": "Категорія",
"CertificateValidation": "Перевірка сертифіката",
"CertificateValidationHelpText": "Змініть суворість перевірки сертифікації HTTPS. Не змінюйте, якщо не розумієте ризики.",
"ChangeCategory": "Змінити категорію",
"BlackholeFolderHelpText": "Папка, у якій {appName} зберігатиме файл {extension}",
"ChangeFileDateHelpText": "Змінити дату файлу під час імпорту/повторного сканування",
"Clear": "Очистити",
"ClearBlocklist": "Очистити список блокувань",
"CloneProfile": "Клонувати профіль",
"DeleteBackup": "Видалити резервну копію",
"DeleteBackupMessageText": "Ви впевнені, що хочете видалити резервну копію \"{name}\"?",
"DeleteCustomFormat": "Видалити спеціальний формат",
"DeleteDownloadClient": "Видалити клієнт завантаження",
"DeleteEmptyFolders": "Видалити порожні папки",
"DeleteImportListMessageText": "Ви впевнені, що хочете видалити список '{name}'?",
"DeleteEpisodesFilesHelpText": "Видалити файли серій і папку серіалів",
"DeleteEpisodesFiles": "Видалити файли епізодів ({episodeFileCount}).",
"Details": "Подробиці",
"DeleteSelectedIndexers": "Видалити індексатор(и)",
"DownloadClientDelugeTorrentStateError": "Deluge повідомляє про помилку",
"DownloadClientDelugeValidationLabelPluginFailure": "Помилка налаштування мітки",
"DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} не зміг додати мітку до {clientName}.",
"DownloadClientDownloadStationProviderMessage": "{appName} не може підключитися до Download Station, якщо у вашому обліковому записі DSM увімкнено 2-факторну автентифікацію",
"DownloadClientDownloadStationValidationApiVersion": "Версія Download Station API не підтримується, має бути принаймні {requiredVersion}. Він підтримує від {minVersion} до {maxVersion}",
"DownloadClientDownloadStationValidationFolderMissingDetail": "Папка '{downloadDir}' не існує, її потрібно створити вручну в спільній папці '{sharedFolder}'.",
"DownloadClientDownloadStationValidationFolderMissing": "Папка не існує",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Ви повинні ввійти до свого Diskstation як {username} та вручну налаштувати його в налаштуваннях DownloadStation у розділі BT/HTTP/FTP/NZB -> Місцезнаходження.",
"DownloadClientFloodSettingsAdditionalTags": "Додаткові теги",
"DownloadClientDownloadStationValidationSharedFolderMissing": "Спільна папка не існує",
"DownloadClientFloodSettingsRemovalInfo": "{appName} оброблятиме автоматичне видалення торрентів на основі поточних критеріїв заповнення в меню «Налаштування» -> «Індексатори»",
"AutoTaggingSpecificationTag": "Тег",
"BlocklistAndSearch": "Чорний список і пошук",
"CalendarLegendEpisodeDownloadedTooltip": "Серія завантажена та відсортована",
"ClickToChangeReleaseType": "Натисніть, щоб змінити тип випуску",
"CustomFormatsSpecificationRegularExpressionHelpText": "Спеціальний формат RegEx не враховує регістр",
"DailyEpisodeFormat": "Формат щоденних серій",
"DailyEpisodeTypeDescription": "Серії, що випускаються щодня або рідше, у яких використовується рік-місяць-день (2023-08-04)",
"DailyEpisodeTypeFormat": "Дата ({format})",
"DatabaseMigration": "Міграція бази даних",
"Dates": "Дати",
"Day": "День",
"DeleteEmptySeriesFoldersHelpText": "Видаліть порожні папки серіалів і сезонів під час сканування диска та під час видалення файлів серій",
"DeleteEpisodeFromDisk": "Видалити серію з диска",
"DeleteNotification": "Видалити сповіщення",
"DeleteNotificationMessageText": "Ви впевнені, що хочете видалити сповіщення '{name}'?",
"DeleteQualityProfileMessageText": "Ви впевнені, що хочете видалити профіль якості '{name}'?",
"DownloadClientOptionsLoadError": "Не вдалося завантажити параметри клієнта завантаження",
"DownloadClientSettingsRecentPriorityEpisodeHelpText": "Пріоритет для використання під час захоплення серій, які вийшли в ефір протягом останніх 14 днів",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "Чи використовувати налаштований макет вмісту qBittorrent, оригінальний макет із торрента чи завжди створювати вкладену папку (qBittorrent 4.3.2+)",
"DownloadClientSettingsInitialStateHelpText": "Початковий стан для торрентів, доданих до {clientName}",
"CalendarLegendEpisodeOnAirTooltip": "Серія зараз в ефірі",
"AutoRedownloadFailed": "Помилка повторного завантаження",
"AutoRedownloadFailedFromInteractiveSearch": "Помилка повторного завантаження з інтерактивного пошуку",
"BindAddress": "Прив'язати адресу",
"BypassDelayIfAboveCustomFormatScoreMinimumScore": "Мінімальна оцінка спеціального формату",
"CheckDownloadClientForDetails": "перевірте клієнт завантаження, щоб дізнатися більше",
"BackupsLoadError": "Не вдалося завантажити резервні копії",
"Date": "Дата",
"AutoTaggingSpecificationStatus": "Статус",
"AutoTaggingSpecificationGenre": "Жанр(и)",
"AutoTaggingSpecificationMaximumYear": "Максимальний рік",
"BlackholeWatchFolderHelpText": "Папка, з якої {appName} має імпортувати завершені завантаження",
"BranchUpdate": "Гілка для оновлення {appName}",
"BrowserReloadRequired": "Потрібно перезавантажити браузер",
"AutoTaggingSpecificationMinimumYear": "Мінімальний рік",
"AutoTaggingSpecificationOriginalLanguage": "Мова",
"AutoTaggingSpecificationQualityProfile": "Профіль Якості",
"CouldNotFindResults": "Не вдалося знайти жодних результатів для '{term}'",
"CustomFormatsSpecificationLanguage": "Мова",
"CustomFormatsSpecificationMaximumSize": "Максимальний розмір",
"CustomFormatsSpecificationReleaseGroup": "Група випуску",
"CustomFormatsSpecificationResolution": "Роздільна здатність",
"CustomFormatsSpecificationSource": "Джерело",
"DefaultNotFoundMessage": "Ви, мабуть, заблукали, тут нічого не видно.",
"DeleteSeriesFolder": "Видалити папку серіалу",
"DeleteSeriesFolderConfirmation": "Папку серіалу `{path}` і весь її вміст буде видалено.",
"DownloadClientFloodSettingsUrlBaseHelpText": "Додає префікс до API Flood, наприклад {url}",
"DownloadClientFreeboxApiError": "API Freebox повернув помилку: {errorDescription}",
"DownloadClientFreeboxSettingsApiUrl": "API URL",
"DownloadClientSettings": "Налаштування клієнта завантаження",
"DownloadClientSettingsOlderPriorityEpisodeHelpText": "Пріоритет для використання під час захоплення серій, які вийшли в ефір понад 14 днів тому",
"AddRootFolderError": "Не вдалося додати кореневу папку",
"AppUpdatedVersion": "{appName} оновлено до версії `{version}`, щоб отримати останні зміни, вам потрібно перезавантажити {appName} ",
"Backups": "Резервні копії",
"AutoRedownloadFailedHelpText": "Автоматичний пошук і спроба завантажити інший випуск",
"BackupIntervalHelpText": "Інтервал між автоматичним резервним копіюванням",
"BeforeUpdate": "Перед оновленням",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation не має спільної папки з назвою '{sharedFolder}', ви впевнені, що вказали її правильно?",
"DownloadClientQbittorrentSettingsUseSslHelpText": "Використовувати безпечне з'єднання. Дивіться параметри -> Web UI -> 'Use HTTPS instead of HTTP' в qBittorrent.",
"DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Спочатку завантажте першу та останню частини (qBittorrent 4.1.0+)",
"DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Завантаження в послідовному порядку (qBittorrent 4.1.0+)",
"CustomFormatsSettingsTriggerInfo": "Спеціальний формат буде застосовано до випуску або файлу, якщо він відповідає принаймні одному з кожного з різних типів вибраних умов.",
"CustomFormatsSpecificationMinimumSize": "Мінімальний розмір",
"CustomFormatsSpecificationMaximumSizeHelpText": "Випуск повинен бути меншим або дорівнювати цьому розміру",
"CustomFormatsSpecificationMinimumSizeHelpText": "Випуск повинен бути більше цього розміру",
"Delete": "Видалити",
"DeleteReleaseProfileMessageText": "Ви впевнені, що хочете видалити цей профіль випуску '{name}'?",
"DeleteRootFolderMessageText": "Ви впевнені, що бажаєте видалити кореневу папку '{path}'?",
"DeleteSelectedDownloadClients": "Видалити клієнт(и) завантаження",
"DockerUpdater": "Оновіть контейнер docker, щоб отримати оновлення",
"DownloadClientQbittorrentValidationCategoryAddFailure": "Помилка налаштування категорії",
"ClearBlocklistMessageText": "Ви впевнені, що бажаєте очистити всі елементи зі списку блокування?",
"ClickToChangeEpisode": "Натисніть, щоб змінити серію",
"ClickToChangeLanguage": "Натисніть, щоб змінити мову",
"CollapseAll": "Закрити все",
"ConnectSettingsSummary": "Сповіщення, підключення до медіасерверів/програвачів і спеціальні сценарії",
"CustomFormatsSettings": "Налаштування спеціальних форматів",
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Необов’язкове розташування для переміщення завершених завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Deluge",
"DownloadClientDelugeSettingsDirectoryHelpText": "Необов’язкове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Deluge",
"DownloadClientSettingsCategorySubFolderHelpText": "Додавання спеціальної категорії для {appName} дозволяє уникнути конфліктів із непов’язаними завантаженнями, не пов’язаними з {appName}. Використання категорії необов’язкове, але настійно рекомендується. Створює підкаталог [category] у вихідному каталозі.",
"DownloadClientQbittorrentValidationCategoryRecommended": "Категорія рекомендована",
"DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent налаштовано на видалення торрентів, коли вони досягають ліміту коефіцієнта спільного використання",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent повідомляє про відсутність файлів",
"DownloadClientSettingsCategoryHelpText": "Додавання спеціальної категорії для {appName} дозволяє уникнути конфліктів із непов’язаними завантаженнями, не пов’язаними з {appName}. Використання категорії необов’язкове, але настійно рекомендується.",
"Donations": "Пожертви",
"DownloadClientSeriesTagHelpText": "Використовуйте цей клієнт завантаження лише для серіалів із принаймні одним відповідним тегом. Залиште поле порожнім для використання з усіма серіалами.",
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Не вдалося зв’язатися з {downloadClientName}. {errorMessage}",
"DownloadClientDelugeValidationLabelPluginInactive": "Плагін міток не активовано",
"DownloadClientAriaSettingsDirectoryHelpText": "Додаткове розташування для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Aria2",
"CustomFormatHelpText": "{appName} оцінює кожен випуск, використовуючи суму балів для відповідності користувацьких форматів. Якщо новий випуск покращить оцінку, з такою ж або кращою якістю, {appName} схопить його.",
"DownloadClientDownloadStationSettingsDirectoryHelpText": "Додаткова спільна папка для розміщення завантажень. Залиште поле порожнім, щоб використовувати стандартне розташування Download Station"
}

View File

@@ -74,7 +74,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
environmentVariables.Add("Sonarr_Release_EpisodeCount", remoteEpisode.Episodes.Count.ToString());
environmentVariables.Add("Sonarr_Release_SeasonNumber", remoteEpisode.Episodes.First().SeasonNumber.ToString());
environmentVariables.Add("Sonarr_Release_EpisodeNumbers", string.Join(",", remoteEpisode.Episodes.Select(e => e.EpisodeNumber)));
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
environmentVariables.Add("Sonarr_EpisodeFile_Id", episodeFile.Id.ToString());
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeCount", episodeFile.Episodes.Value.Count.ToString());
environmentVariables.Add("Sonarr_EpisodeFile_RelativePath", episodeFile.RelativePath);
@@ -186,7 +186,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
environmentVariables.Add("Sonarr_EpisodeFile_Ids", string.Join(",", renamedFiles.Select(e => e.EpisodeFile.Id)));
environmentVariables.Add("Sonarr_EpisodeFile_RelativePaths", string.Join("|", renamedFiles.Select(e => e.EpisodeFile.RelativePath)));
environmentVariables.Add("Sonarr_EpisodeFile_Paths", string.Join("|", renamedFiles.Select(e => e.EpisodeFile.Path)));
@@ -218,7 +218,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
environmentVariables.Add("Sonarr_EpisodeFile_Id", episodeFile.Id.ToString());
environmentVariables.Add("Sonarr_EpisodeFile_EpisodeCount", episodeFile.Episodes.Value.Count.ToString());
environmentVariables.Add("Sonarr_EpisodeFile_RelativePath", episodeFile.RelativePath);
@@ -257,7 +257,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
ExecuteScript(environmentVariables);
}
@@ -281,7 +281,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
environmentVariables.Add("Sonarr_Series_DeletedFiles", deleteMessage.DeletedFiles.ToString());
ExecuteScript(environmentVariables);
@@ -350,7 +350,7 @@ namespace NzbDrone.Core.Notifications.CustomScript
environmentVariables.Add("Sonarr_Series_Year", series.Year.ToString());
environmentVariables.Add("Sonarr_Series_OriginalLanguage", IsoLanguages.Get(series.OriginalLanguage).ThreeLetterCode);
environmentVariables.Add("Sonarr_Series_Genres", string.Join("|", series.Genres));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", series.Tags.Select(t => _tagRepository.Get(t).Label)));
environmentVariables.Add("Sonarr_Series_Tags", string.Join("|", GetTagLabels(series)));
environmentVariables.Add("Sonarr_Download_Client", message.DownloadClientInfo?.Name ?? string.Empty);
environmentVariables.Add("Sonarr_Download_Client_Type", message.DownloadClientInfo?.Type ?? string.Empty);
environmentVariables.Add("Sonarr_Download_Id", message.DownloadId ?? string.Empty);
@@ -411,5 +411,14 @@ namespace NzbDrone.Core.Notifications.CustomScript
{
return possibleParent.IsParentPath(path);
}
private List<string> GetTagLabels(Series series)
{
return _tagRepository.GetTags(series.Tags)
.Select(s => s.Label)
.Where(l => l.IsNotNullOrWhiteSpace())
.OrderBy(l => l)
.ToList();
}
}
}

View File

@@ -5,6 +5,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications.Webhook;
using NzbDrone.Core.Tags;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation;
@@ -14,8 +15,8 @@ namespace NzbDrone.Core.Notifications.Notifiarr
{
private readonly INotifiarrProxy _proxy;
public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService)
: base(configFileProvider, configService, localizationService)
public Notifiarr(INotifiarrProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository)
: base(configFileProvider, configService, localizationService, tagRepository)
{
_proxy = proxy;
}

View File

@@ -30,6 +30,6 @@ namespace NzbDrone.Core.Notifications
public bool SupportsOnApplicationUpdate { get; set; }
public bool SupportsOnManualInteractionRequired { get; set; }
public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired;
public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade) || OnRename || OnSeriesAdd || OnSeriesDelete || OnEpisodeFileDelete || (OnEpisodeFileDelete && OnEpisodeFileDeleteForUpgrade) || OnHealthIssue || OnHealthRestored || OnApplicationUpdate || OnManualInteractionRequired;
}
}

View File

@@ -4,6 +4,7 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tags;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation;
@@ -13,8 +14,8 @@ namespace NzbDrone.Core.Notifications.Webhook
{
private readonly IWebhookProxy _proxy;
public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService)
: base(configFileProvider, configService, localizationService)
public Webhook(IWebhookProxy proxy, IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository)
: base(configFileProvider, configService, localizationService, tagRepository)
{
_proxy = proxy;
}

View File

@@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Tags;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
@@ -15,12 +17,14 @@ namespace NzbDrone.Core.Notifications.Webhook
private readonly IConfigFileProvider _configFileProvider;
private readonly IConfigService _configService;
protected readonly ILocalizationService _localizationService;
private readonly ITagRepository _tagRepository;
protected WebhookBase(IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService)
protected WebhookBase(IConfigFileProvider configFileProvider, IConfigService configService, ILocalizationService localizationService, ITagRepository tagRepository)
{
_configFileProvider = configFileProvider;
_configService = configService;
_localizationService = localizationService;
_tagRepository = tagRepository;
}
protected WebhookGrabPayload BuildOnGrabPayload(GrabMessage message)
@@ -33,7 +37,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.Grab,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(message.Series),
Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)),
Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)),
Release = new WebhookRelease(quality, remoteEpisode),
DownloadClient = message.DownloadClientName,
@@ -52,7 +56,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.Download,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(message.Series),
Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)),
Episodes = episodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)),
EpisodeFile = new WebhookEpisodeFile(episodeFile),
Release = new WebhookGrabbedRelease(message.Release),
@@ -82,7 +86,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.EpisodeFileDelete,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(deleteMessage.Series),
Series = new WebhookSeries(deleteMessage.Series, GetTagLabels(deleteMessage.Series)),
Episodes = deleteMessage.EpisodeFile.Episodes.Value.ConvertAll(x => new WebhookEpisode(x)),
EpisodeFile = new WebhookEpisodeFile(deleteMessage.EpisodeFile),
DeleteReason = deleteMessage.Reason
@@ -96,7 +100,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.SeriesAdd,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(addMessage.Series),
Series = new WebhookSeries(addMessage.Series, GetTagLabels(addMessage.Series)),
};
}
@@ -107,7 +111,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.SeriesDelete,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(deleteMessage.Series),
Series = new WebhookSeries(deleteMessage.Series, GetTagLabels(deleteMessage.Series)),
DeletedFiles = deleteMessage.DeletedFiles
};
}
@@ -119,7 +123,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.Rename,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(series),
Series = new WebhookSeries(series, GetTagLabels(series)),
RenamedEpisodeFiles = renamedFiles.ConvertAll(x => new WebhookRenamedEpisodeFile(x))
};
}
@@ -172,7 +176,7 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.ManualInteractionRequired,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries(message.Series),
Series = new WebhookSeries(message.Series, GetTagLabels(message.Series)),
Episodes = remoteEpisode.Episodes.ConvertAll(x => new WebhookEpisode(x)),
DownloadInfo = new WebhookDownloadClientItem(quality, message.TrackedDownload.DownloadItem),
DownloadClient = message.DownloadClientInfo?.Name,
@@ -192,12 +196,13 @@ namespace NzbDrone.Core.Notifications.Webhook
EventType = WebhookEventType.Test,
InstanceName = _configFileProvider.InstanceName,
ApplicationUrl = _configService.ApplicationUrl,
Series = new WebhookSeries()
Series = new WebhookSeries
{
Id = 1,
Title = "Test Title",
Path = "C:\\testpath",
TvdbId = 1234
TvdbId = 1234,
Tags = new List<string> { "test-tag" }
},
Episodes = new List<WebhookEpisode>()
{
@@ -211,5 +216,14 @@ namespace NzbDrone.Core.Notifications.Webhook
}
};
}
private List<string> GetTagLabels(Series series)
{
return _tagRepository.GetTags(series.Tags)
.Select(s => s.Label)
.Where(l => l.IsNotNullOrWhiteSpace())
.OrderBy(l => l)
.ToList();
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Notifications.Webhook
@@ -13,12 +14,13 @@ namespace NzbDrone.Core.Notifications.Webhook
public string ImdbId { get; set; }
public SeriesTypes Type { get; set; }
public int Year { get; set; }
public List<string> Tags { get; set; }
public WebhookSeries()
{
}
public WebhookSeries(Series series)
public WebhookSeries(Series series, List<string> tags)
{
Id = series.Id;
Title = series.Title;
@@ -29,6 +31,7 @@ namespace NzbDrone.Core.Notifications.Webhook
ImdbId = series.ImdbId;
Type = series.SeriesType;
Year = series.Year;
Tags = tags;
}
}
}

View File

@@ -215,7 +215,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Partial season pack
new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e)(?<seasonpart>\d{1,2}(?!\d+)))+)",
new Regex(@"^(?<title>.+?)(?:\W+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))\W+(?:(?:(?:Part|Vol)\W?|(?<!\d+\W+)e|p)(?<seasonpart>\d{1,2}(?!\d+)))+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Anime - 4 digit absolute episode number
@@ -339,7 +339,7 @@ namespace NzbDrone.Core.Parser
// 4 digit episode number
// Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc)
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)",
new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]|\d{1,2}-))+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (2018.04.28)
@@ -350,7 +350,11 @@ namespace NzbDrone.Core.Parser
new Regex(@"^(?<title>.+?)[_. ](?<absoluteepisode>\d{1,4})(?:[_. ]+)(?:BLM|B[oö]l[uü]m)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (04.28.2018)
new Regex(@"^(?<title>.+?)?\W*(?<airmonth>[0-1][0-9])[-_. ]+(?<airday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)",
new Regex(@"^(?<title>.+?)?\W*(?<ambiguousairmonth>[0-1][0-9])[-_. ]+(?<ambiguousairday>[0-3][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (28.04.2018)
new Regex(@"^(?<title>.+?)?\W*(?<ambiguousairday>[0-3][0-9])[-_. ]+(?<ambiguousairmonth>[0-1][0-9])[-_. ]+(?<airyear>\d{4})(?!\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Episodes with airdate (20180428)
@@ -362,7 +366,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Supports 1103/1113 naming
new Regex(@"^(?<title>.+?)?(?:(?:[-_. ](?<![()\[!]))*(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9])(?!p|i|\d+|\)|\]|\W\d+|\W(?:e|ep|x)\d+)))+([-_. ]+|$)(?!\\)",
new Regex(@"^(?<title>.+?)?(?:(?:[-_. ](?<![()\[!]))*(?<!\d{1,2}-)(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)(?:[1-9][0-9]|[0][1-9])(?!p|i|\d+|\)|\]|\W\d+|\W(?:e|ep|x)\d+)))+([-_. ]+|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Dutch/Flemish release titles
@@ -1128,15 +1132,36 @@ namespace NzbDrone.Core.Parser
else
{
// Try to Parse as a daily show
var airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value);
var airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value);
// Swap day and month if month is bigger than 12 (scene fail)
if (airmonth > 12)
var airmonth = 0;
var airday = 0;
if (matchCollection[0].Groups["ambiguousairmonth"].Success &&
matchCollection[0].Groups["ambiguousairday"].Success)
{
var tempDay = airday;
airday = airmonth;
airmonth = tempDay;
var ambiguousAirMonth = Convert.ToInt32(matchCollection[0].Groups["ambiguousairmonth"].Value);
var ambiguousAirDay = Convert.ToInt32(matchCollection[0].Groups["ambiguousairday"].Value);
if (ambiguousAirDay <= 12 && ambiguousAirMonth <= 12)
{
throw new InvalidDateException("Ambiguous Date, cannot validate month and day with {0} and {1}", ambiguousAirMonth, ambiguousAirDay);
}
airmonth = ambiguousAirMonth;
airday = ambiguousAirDay;
}
else
{
airmonth = Convert.ToInt32(matchCollection[0].Groups["airmonth"].Value);
airday = Convert.ToInt32(matchCollection[0].Groups["airday"].Value);
// Swap day and month if month is bigger than 12 (scene fail)
if (airmonth > 12)
{
var tempDay = airday;
airday = airmonth;
airmonth = tempDay;
}
}
DateTime airDate;

View File

@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex RealRegex = new (@"\b(?<real>REAL)\b",
RegexOptions.Compiled);
private static readonly Regex ResolutionRegex = new (@"\b(?:(?<R360p>360p)|(?<R480p>480p|640x480|848x480)|(?<R540p>540p)|(?<R576p>576p)|(?<R720p>720p|1280x720|960p)|(?<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|(?<R2160p>2160p|3840x2160|4k[-_. ](?:UHD|HEVC|BD|H265)|(?:UHD|HEVC|BD|H265)[-_. ]4k))\b",
private static readonly Regex ResolutionRegex = new (@"\b(?:(?<R360p>360p)|(?<R480p>480p|480i|640x480|848x480)|(?<R540p>540p)|(?<R576p>576p)|(?<R720p>720p|1280x720|960p)|(?<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|(?<R2160p>2160p|3840x2160|4k[-_. ](?:UHD|HEVC|BD|H265)|(?:UHD|HEVC|BD|H265)[-_. ]4k))\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
// Handle cases where no resolution is in the release name (assume if UHD then 4k) or resolution is non-standard
@@ -311,6 +311,35 @@ namespace NzbDrone.Core.Parser
}
}
if (sourceMatch == null && remuxMatch && resolution != Resolution.Unknown)
{
result.SourceDetectionSource = QualityDetectionSource.Unknown;
if (resolution == Resolution.R480P)
{
result.Quality = Quality.Bluray480p;
return result;
}
if (resolution == Resolution.R720p)
{
result.Quality = Quality.Bluray720p;
return result;
}
if (resolution == Resolution.R2160p)
{
result.Quality = Quality.Bluray2160pRemux;
return result;
}
if (resolution == Resolution.R1080p)
{
result.Quality = Quality.Bluray1080pRemux;
return result;
}
}
// Anime Bluray matching
if (AnimeBlurayRegex.Match(normalizedName).Success)
{

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
@@ -28,7 +29,7 @@ namespace NzbDrone.Core.SeriesStats
try
{
if (!DateTime.TryParse(NextAiringString, out nextAiring))
if (!DateTime.TryParse(NextAiringString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out nextAiring))
{
return null;
}
@@ -51,7 +52,7 @@ namespace NzbDrone.Core.SeriesStats
try
{
if (!DateTime.TryParse(PreviousAiringString, out previousAiring))
if (!DateTime.TryParse(PreviousAiringString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out previousAiring))
{
return null;
}
@@ -74,7 +75,7 @@ namespace NzbDrone.Core.SeriesStats
try
{
if (!DateTime.TryParse(LastAiredString, out lastAired))
if (!DateTime.TryParse(LastAiredString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out lastAired))
{
return null;
}

View File

@@ -7,83 +7,14 @@ namespace NzbDrone.Core.SeriesStats
public class SeriesStatistics : ResultSet
{
public int SeriesId { get; set; }
public string NextAiringString { get; set; }
public string PreviousAiringString { get; set; }
public string LastAiredString { get; set; }
public DateTime? NextAiring { get; set; }
public DateTime? PreviousAiring { get; set; }
public DateTime? LastAired { get; set; }
public int EpisodeFileCount { get; set; }
public int EpisodeCount { get; set; }
public int TotalEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public List<string> ReleaseGroups { get; set; }
public List<SeasonStatistics> SeasonStatistics { get; set; }
public DateTime? NextAiring
{
get
{
DateTime nextAiring;
try
{
if (!DateTime.TryParse(NextAiringString, out nextAiring))
{
return null;
}
}
catch (ArgumentOutOfRangeException)
{
// GHI 3518: Can throw on mono (6.x?) despite being a Try*
return null;
}
return nextAiring;
}
}
public DateTime? PreviousAiring
{
get
{
DateTime previousAiring;
try
{
if (!DateTime.TryParse(PreviousAiringString, out previousAiring))
{
return null;
}
}
catch (ArgumentOutOfRangeException)
{
// GHI 3518: Can throw on mono (6.x?) despite being a Try*
return null;
}
return previousAiring;
}
}
public DateTime? LastAired
{
get
{
DateTime lastAired;
try
{
if (!DateTime.TryParse(LastAiredString, out lastAired))
{
return null;
}
}
catch (ArgumentOutOfRangeException)
{
// GHI 3518: Can throw on mono (6.x?) despite being a Try*
return null;
}
return lastAired;
}
}
}
}

View File

@@ -40,23 +40,23 @@ namespace NzbDrone.Core.SeriesStats
private SeriesStatistics MapSeriesStatistics(List<SeasonStatistics> seasonStatistics)
{
var seriesStatistics = new SeriesStatistics
{
SeasonStatistics = seasonStatistics,
SeriesId = seasonStatistics.First().SeriesId,
EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount),
EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount),
TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount),
SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk),
ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList()
};
{
SeasonStatistics = seasonStatistics,
SeriesId = seasonStatistics.First().SeriesId,
EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount),
EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount),
TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount),
SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk),
ReleaseGroups = seasonStatistics.SelectMany(s => s.ReleaseGroups).Distinct().ToList()
};
var nextAiring = seasonStatistics.Where(s => s.NextAiring != null).MinBy(s => s.NextAiring);
var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null).MaxBy(s => s.PreviousAiring);
var lastAired = seasonStatistics.Where(s => s.SeasonNumber > 0 && s.LastAired != null).MaxBy(s => s.LastAired);
seriesStatistics.NextAiringString = nextAiring?.NextAiringString;
seriesStatistics.PreviousAiringString = previousAiring?.PreviousAiringString;
seriesStatistics.LastAiredString = lastAired?.LastAiredString;
seriesStatistics.NextAiring = nextAiring?.NextAiring;
seriesStatistics.PreviousAiring = previousAiring?.PreviousAiring;
seriesStatistics.LastAired = lastAired?.LastAired;
return seriesStatistics;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
@@ -9,6 +10,7 @@ namespace NzbDrone.Core.Tags
{
Tag GetByLabel(string label);
Tag FindByLabel(string label);
List<Tag> GetTags(HashSet<int> tagIds);
}
public class TagRepository : BasicRepository<Tag>, ITagRepository
@@ -34,5 +36,10 @@ namespace NzbDrone.Core.Tags
{
return Query(c => c.Label == label).SingleOrDefault();
}
public List<Tag> GetTags(HashSet<int> tagIds)
{
return Query(t => tagIds.Contains(t.Id));
}
}
}

View File

@@ -23,7 +23,6 @@ using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Options;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore.Extensions;
using Sonarr.Http.ClientSchema;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions;
@@ -171,8 +170,6 @@ namespace NzbDrone.Host
{
c.AddDummyLogDatabase();
}
SchemaBuilder.Initialize(c);
})
.ConfigureServices(services =>
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using DryIoc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
@@ -26,6 +27,7 @@ using NzbDrone.SignalR;
using Sonarr.Api.V3.System;
using Sonarr.Http;
using Sonarr.Http.Authentication;
using Sonarr.Http.ClientSchema;
using Sonarr.Http.ErrorManagement;
using Sonarr.Http.Frontend;
using Sonarr.Http.Middleware;
@@ -193,6 +195,7 @@ namespace NzbDrone.Host
}
public void Configure(IApplicationBuilder app,
IContainer container,
IStartupContext startupContext,
Lazy<IMainDatabase> mainDatabaseFactory,
Lazy<ILogDatabase> logDatabaseFactory,
@@ -227,6 +230,8 @@ namespace NzbDrone.Host
dbTarget.Register();
}
SchemaBuilder.Initialize(container);
if (OsInfo.IsNotWindows)
{
Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null;

View File

@@ -1,7 +1,9 @@
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Indexers;
using Sonarr.Http;
using Sonarr.Http.Extensions;
using Sonarr.Http.REST.Attributes;
@@ -23,12 +25,22 @@ namespace Sonarr.Api.V3.Blocklist
[HttpGet]
[Produces("application/json")]
public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestResource paging)
public PagingResource<BlocklistResource> GetBlocklist([FromQuery] PagingRequestResource paging, [FromQuery] int[] seriesIds = null, [FromQuery] DownloadProtocol[] protocols = null)
{
var pagingResource = new PagingResource<BlocklistResource>(paging);
var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending);
return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator));
if (seriesIds?.Any() == true)
{
pagingSpec.FilterExpressions.Add(b => seriesIds.Contains(b.SeriesId));
}
if (protocols?.Any() == true)
{
pagingSpec.FilterExpressions.Add(b => protocols.Contains(b.Protocol));
}
return pagingSpec.ApplyToPage(b => _blocklistService.Paged(pagingSpec), b => BlocklistResourceMapper.MapToResource(b, _formatCalculator));
}
[RestDeleteById]

View File

@@ -413,6 +413,27 @@
"schema": {
"$ref": "#/components/schemas/SortDirection"
}
},
{
"name": "seriesIds",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
},
{
"name": "protocols",
"in": "query",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DownloadProtocol"
}
}
}
],
"responses": {

View File

@@ -162,7 +162,7 @@ namespace Sonarr.Http.ClientSchema
field.Hidden = fieldAttribute.Hidden.ToString().FirstCharToLower();
}
if (fieldAttribute.Type is FieldType.Number && propertyInfo.PropertyType == typeof(double))
if (fieldAttribute.Type is FieldType.Number && (propertyInfo.PropertyType == typeof(double) || propertyInfo.PropertyType == typeof(double?)))
{
field.IsFloat = true;
}

View File

@@ -39,7 +39,7 @@ namespace Sonarr.Http.Frontend.Mappers
return stream;
}
protected string GetHtmlText()
protected virtual string GetHtmlText()
{
if (RuntimeInfo.IsProduction && _generatedContent != null)
{

View File

@@ -9,6 +9,8 @@ namespace Sonarr.Http.Frontend.Mappers
{
public class LoginHtmlMapper : HtmlMapperBase
{
private readonly IConfigFileProvider _configFileProvider;
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
@@ -16,6 +18,7 @@ namespace Sonarr.Http.Frontend.Mappers
Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger)
{
_configFileProvider = configFileProvider;
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
UrlBase = configFileProvider.UrlBase;
}
@@ -29,5 +32,15 @@ namespace Sonarr.Http.Frontend.Mappers
{
return resourceUrl.StartsWith("/login");
}
protected override string GetHtmlText()
{
var html = base.GetHtmlText();
var theme = _configFileProvider.Theme;
html = html.Replace("_THEME_", theme);
return html;
}
}
}