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

Compare commits

..

20 Commits

Author SHA1 Message Date
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
Bogdan
47ba002806 Fixed: Indexer flags for torrent release pushes 2024-05-04 18:56:52 -07:00
Mark McDowall
ba88185dea New: Treat batch releases with total episode count as full season release
Closes #6757
2024-05-04 18:56:15 -07:00
Mark McDowall
e24ce40eb8 Fixed: History with unknown episode
Closes #6782
2024-05-04 18:55:56 -07:00
Stevie Robinson
8be8c7f89c Add missing translation key 2024-05-04 18:55:44 -07:00
Bogdan
7166a6c019 Parameter binding for API requests 2024-05-04 18:55:12 -07:00
Mark McDowall
3fbe436138 Forward X-Forwarded-Host header
Closes #6764
2024-05-04 18:54:55 -07:00
Jared
92eab4b2e2 New: Config file setting to disable log database
Closes #6743
2024-05-04 18:54:42 -07:00
Mika
23c741fd00 Add file-count for Transmission RPC 2024-05-04 18:53:47 -07:00
Weblate
8ddf46113b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-05-04 18:53:15 -07:00
83 changed files with 996 additions and 356 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

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

@@ -86,6 +86,10 @@ class SeriesHistoryRow extends Component {
const EpisodeComponent = fullSeries ? SeasonEpisodeNumber : EpisodeNumber;
if (!series || !episode) {
return null;
}
return (
<TableRow>
<HistoryEventTypeCell

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

@@ -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,17 @@
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);
}
.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 +131,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 +142,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 +166,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 +292,59 @@
loginFailedDiv.classList.remove("hidden");
}
var light = {
white: '#fff',
pageBackground: '#f5f7fa',
textColor: '#656565',
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: '#656565',
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

@@ -30,6 +30,7 @@ namespace NzbDrone.Common.Test
.AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddDummyDatabase()
.AddDummyLogDatabase()
.AddStartupContext(new StartupContext("first", "second"));
container.RegisterInstance(new Mock<IHostLifetime>().Object);

View File

@@ -11,4 +11,5 @@ public class LogOptions
public string SyslogServer { get; set; }
public int? SyslogPort { get; set; }
public string SyslogLevel { get; set; }
public bool? DbEnabled { get; set; }
}

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

@@ -31,6 +31,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series.Stagione.3.HDTV.XviD-NOTAG", "Series", 3)]
[TestCase("Series.Stagione.3.HDTV.XviD-NOTAG", "Series", 3)]
[TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)]
[TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)]
[TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)]
public void should_parse_full_season_release(string postTitle, string title, int season)
{
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

@@ -54,6 +54,7 @@ namespace NzbDrone.Core.Configuration
string SyslogServer { get; }
int SyslogPort { get; }
string SyslogLevel { get; }
bool LogDbEnabled { get; }
string Theme { get; }
string PostgresHost { get; }
int PostgresPort { get; }
@@ -230,7 +231,7 @@ namespace NzbDrone.Core.Configuration
public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "sonarr-main", persist: false);
public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "sonarr-log", persist: false);
public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false);
public bool LogDbEnabled => _logOptions.DbEnabled ?? GetValueBoolean("LogDbEnabled", true, persist: false);
public bool LogSql => _logOptions.Sql ?? GetValueBoolean("LogSql", false, persist: false);
public int LogRotate => _logOptions.Rotate ?? GetValueInt("LogRotate", 50, persist: false);
public bool FilterSentryEvents => _logOptions.FilterSentryEvents ?? GetValueBoolean("FilterSentryEvents", true, persist: false);

View File

@@ -8,6 +8,12 @@ namespace NzbDrone.Core.Datastore.Extensions
public static IContainer AddDatabase(this IContainer container)
{
container.RegisterDelegate<IDbFactory, IMainDatabase>(f => new MainDatabase(f.Create()), Reuse.Singleton);
return container;
}
public static IContainer AddLogDatabase(this IContainer container)
{
container.RegisterDelegate<IDbFactory, ILogDatabase>(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton);
return container;
@@ -16,6 +22,12 @@ namespace NzbDrone.Core.Datastore.Extensions
public static IContainer AddDummyDatabase(this IContainer container)
{
container.RegisterInstance<IMainDatabase>(new MainDatabase(null));
return container;
}
public static IContainer AddDummyLogDatabase(this IContainer container)
{
container.RegisterInstance<ILogDatabase>(new LogDatabase(null));
return container;

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

@@ -178,7 +178,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
"seedRatioMode",
"seedIdleLimit",
"seedIdleMode",
"fileCount"
"fileCount",
"file-count"
};
var arguments = new Dictionary<string, object>();

View File

@@ -1,4 +1,6 @@
namespace NzbDrone.Core.Download.Clients.Transmission
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.Transmission
{
public class TransmissionTorrent
{
@@ -11,8 +13,8 @@
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; }
@@ -20,6 +22,12 @@
public int SeedRatioMode { get; set; }
public long SeedIdleLimit { get; set; }
public int SeedIdleMode { get; set; }
public int FileCount { get; set; }
public int FileCount => TransmissionFileCount ?? VuzeFileCount ?? 0;
[JsonProperty(PropertyName = "file-count")]
public int? TransmissionFileCount { get; set; }
[JsonProperty(PropertyName = "fileCount")]
public int? VuzeFileCount { 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

@@ -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}",
@@ -473,6 +474,7 @@
"DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent cannot resolve magnet link with DHT disabled",
"DownloadClientQbittorrentTorrentStateError": "qBittorrent is reporting an error",
"DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent is downloading metadata",
"DownloadClientQbittorrentTorrentStateMissingFiles": "qBittorrent is reporting missing files",
"DownloadClientQbittorrentTorrentStatePathError": "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?",
"DownloadClientQbittorrentTorrentStateStalled": "The download is stalled with no connections",
"DownloadClientQbittorrentTorrentStateUnknown": "Unknown download state: {state}",
@@ -750,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",
@@ -866,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",
@@ -975,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",
@@ -1789,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",
@@ -1803,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

@@ -1467,7 +1467,7 @@
"SeriesTypes": "Tipos de serie",
"SeriesTypesHelpText": "El tipo de serie es usado para renombrar, analizar y buscar",
"SingleEpisodeInvalidFormat": "Episodio individual: Formato inválido",
"SslCertPasswordHelpText": "Contraseña para el archivo pfx",
"SslCertPasswordHelpText": "Contraseña para archivo pfx",
"SslPort": "Puerto SSL",
"StandardEpisodeFormat": "Formato de episodio estándar",
"StartProcessing": "Iniciar procesamiento",
@@ -1668,7 +1668,7 @@
"Uppercase": "Mayúsculas",
"SeriesDetailsRuntime": "{runtime} minutos",
"ShowBannersHelpText": "Muestra banners en lugar de títulos",
"SslCertPathHelpText": "Ruta al archivo pfx",
"SslCertPathHelpText": "Ruta del archivo pfx",
"Umask750Description": "{octal} - Usuario escribe, Grupo lee",
"UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío",
"UpdateAll": "Actualizar todo",

View File

@@ -846,7 +846,7 @@
"SpecialsFolderFormat": "Formato da Pasta para Especiais",
"SslCertPassword": "Senha do Certificado SSL",
"SslCertPasswordHelpText": "Senha para arquivo pfx",
"SslCertPath": "Caminho do certificado SSL",
"SslCertPath": "Caminho do Certificado SSL",
"SslCertPathHelpText": "Caminho para o arquivo pfx",
"SslPort": "Porta SSL",
"StandardEpisodeFormat": "Formato do Episódio Padrão",

View File

@@ -10,7 +10,7 @@
"AddIndexerImplementation": "Yeni Dizin Ekle - {implementationName}",
"EditIndexerImplementation": "Koşul Ekle - {implementationName}",
"AddToDownloadQueue": "İndirme kuyruğuna ekleyin",
"AddedToDownloadQueue": "İndirme sırasına eklendi",
"AddedToDownloadQueue": "İndirme kuyruğuna eklendi",
"AllTitles": "Tüm Başlıklar",
"AbsoluteEpisodeNumbers": "Mutlak Bölüm Numaraları",
"Actions": "Eylemler",
@@ -34,7 +34,7 @@
"AddIndexer": "Dizin Oluşturucu Ekle",
"AddNewSeriesSearchForMissingEpisodes": "Kayıp bölümleri aramaya başlayın",
"AddNotificationError": "Yeni bir bildirim eklenemiyor, lütfen tekrar deneyin.",
"AddReleaseProfile": "Sürüm Profili Ekle",
"AddReleaseProfile": "Yayın Profili Ekle",
"AddRemotePathMapping": "Uzak Yol Eşleme Ekleme",
"AddRootFolder": "Kök Klasör Ekle",
"AddSeriesWithTitle": "{title} Ekleyin",
@@ -69,7 +69,7 @@
"CountImportListsSelected": "{count} içe aktarma listesi seçildi",
"CustomFormatsSpecificationFlag": "Bayrak",
"ClickToChangeIndexerFlags": "Dizin oluşturucu bayraklarını değiştirmek için tıklayın",
"ClickToChangeReleaseGroup": "Sürüm grubunu 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",
"ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin",
@@ -80,7 +80,7 @@
"AuthenticationMethodHelpTextWarning": "Lütfen geçerli bir kimlik doğrulama yöntemi seçin",
"AutoTaggingRequiredHelpText": "Otomatik etiketleme kuralının uygulanabilmesi için bu {implementationName} koşulunun eşleşmesi gerekir. Aksi takdirde tek bir {implementationName} eşleşmesi yeterlidir.",
"BlocklistLoadError": "Engellenenler listesi yüklenemiyor",
"BypassDelayIfHighestQualityHelpText": "Tercih edilen protokolle kalite profilinde en yüksek etkin kaliteye sahip sürüm olduğunda gecikmeyi atlayın",
"BypassDelayIfHighestQualityHelpText": "Tercih edilen protokolle kalite profilinde en yüksek etkin kaliteye sahip yayın olduğunda gecikmeyi atlayın",
"ConnectionLostToBackend": "{appName}'ın arka uçla bağlantısı kesildi ve işlevselliğin geri kazanılması için yeniden yüklenmesi gerekecek.",
"CustomFormatJson": "Özel JSON Formatı",
"AutomaticAdd": "Otomatik Ekle",
@@ -104,8 +104,8 @@
"BlocklistMultipleOnlyHint": "Yedekleri aramadan engelleme listesi",
"BlocklistOnly": "Yalnızca Engellenenler Listesi",
"BlocklistOnlyHint": "Yenisini aramadan engelleme listesi",
"BlocklistReleaseHelpText": "Bu sürümün {appName} tarafından RSS veya Otomatik Arama yoluyla yeniden indirilmesi engelleniyor",
"BypassDelayIfAboveCustomFormatScoreHelpText": "Sürümün puanı, yapılandırılan minimum özel format puanından yüksek olduğunda bypass'ı etkinleştirin",
"BlocklistReleaseHelpText": "Bu yayın {appName} tarafından RSS veya Otomatik Arama yoluyla yeniden indirilmesi engelleniyor",
"BypassDelayIfAboveCustomFormatScoreHelpText": "Yayının puanı, yapılandırılan minimum özel format puanından yüksek olduğunda bypass'ı etkinleştirin",
"BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Tercih edilen protokolde gecikmeyi atlamak için gereken Minimum Özel Format Puanı",
"BypassDelayIfHighestQuality": "En Yüksek Kalitedeyse Atla",
"ChangeCategory": "Kategoriyi Değiştir",
@@ -145,7 +145,7 @@
"BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'",
"CloneAutoTag": "Otomatik Etiketi Klonla",
"Dash": "Çizgi",
"DeleteReleaseProfileMessageText": "'{name}' bu sürüm profilini silmek istediğinizden emin misiniz?",
"DeleteReleaseProfileMessageText": "'{name}' bu yayımlama profilini silmek istediğinizden emin misiniz?",
"DownloadClientFreeboxApiError": "Freebox API'si şu hatayı döndürdü: {errorDescription}",
"DeleteSelectedDownloadClients": "İndirme İstemcilerini Sil",
"DeleteSelectedDownloadClientsMessageText": "Seçilen {count} indirme istemcisini silmek istediğinizden emin misiniz?",
@@ -154,7 +154,7 @@
"DeletedReasonUpgrade": "Bir yükseltmeyi içe aktarmak için dosya silindi",
"DelayMinutes": "{delay} Dakika",
"DeleteImportListMessageText": "'{name}' listesini silmek istediğinizden emin misiniz?",
"DeleteReleaseProfile": "Sürüm Profilini Sil",
"DeleteReleaseProfile": "Yayımlama Profilini Sil",
"DeleteSelectedIndexers": "Dizin Oluşturucuları Sil",
"Directory": "Rehber",
"Donate": "Bağış yap",
@@ -165,7 +165,7 @@
"DownloadClientFloodSettingsTagsHelpText": "Bir indirme işleminin başlangıç etiketleri. Bir indirmenin tanınabilmesi için tüm başlangıç etiketlerine sahip olması gerekir. Bu, ilgisiz indirmelerle çakışmaları önler.",
"DownloadClientAriaSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum, varsayılan Aria2 konumunu kullanmak için boş bırakın",
"DefaultNameCopiedProfile": "{name} - Kopyala",
"DeleteAutoTag": "Etiketi Otomatik Sil",
"DeleteAutoTag": "Etiketi Otomatik Sil",
"DeleteCondition": "Koşulu Sil",
"DeleteDelayProfileMessageText": "Bu gecikme profilini silmek istediğinizden emin misiniz?",
"DeleteRootFolder": "Kök Klasörü Sil",
@@ -195,7 +195,7 @@
"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 sürümler için eşitlik bozucu olarak sürümler alınırken kullanılan {appName}, RSS Senkronizasyonu ve Arama için etkinleştirilmiş tüm dizin oluşturucuları kullanmaya devam edecek",
"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",
"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.",
@@ -208,7 +208,7 @@
"DeleteAutoTagHelpText": "'{name}' etiketini otomatik silmek istediğinizden emin misiniz?",
"DownloadClientDelugeSettingsUrlBaseHelpText": "Deluge json URL'sine bir önek ekler, bkz. {url}",
"DownloadClientFreeboxSettingsPortHelpText": "Freebox arayüzüne erişim için kullanılan bağlantı noktası, varsayılan olarak '{port}' şeklindedir",
"DownloadClientFreeboxUnableToReachFreebox": "Freebox API'sine ulaşılamıyor. 'Ana Bilgisayar', 'Bağlantı Noktası' veya 'SSL Kullan' ayarlarını doğrulayın. (Hata: {istisnaMessage})",
"DownloadClientFreeboxUnableToReachFreebox": "Freebox API'sine ulaşılamıyor. 'Ana Bilgisayar', 'Bağlantı Noktası' veya 'SSL Kullan' ayarlarını doğrulayın. (Hata: {exceptionMessage})",
"CustomFormatsSettingsTriggerInfo": "Bir yayına veya dosyaya, seçilen farklı koşul türlerinden en az biriyle eşleştiğinde Özel Format uygulanacaktır.",
"Default": "Varsayılan",
"DeleteSelectedImportListsMessageText": "Seçilen {count} içe aktarma listesini silmek istediğinizden emin misiniz?",
@@ -218,16 +218,16 @@
"DownloadClientDelugeSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı konum; varsayılan Deluge konumunu kullanmak için boş bırakın",
"DownloadClientDownloadStationSettingsDirectoryHelpText": "İndirilenlerin yerleştirileceği isteğe bağlı paylaşımlı klasör, varsayılan Download Station konumunu kullanmak için boş bırakın",
"ApiKey": "API Anahtarı",
"Analytics": "Analiz",
"Analytics": "Analitik",
"All": "Hepsi",
"AppDataLocationHealthCheckMessage": "Güncellemede AppData'nın silinmesini önlemek için güncelleme mümkün olmayacak",
"AppDataLocationHealthCheckMessage": "Güncelleme sırasında AppData'nın silinmesini önlemek için güncelleme yapılmayacaktır",
"AnalyticsEnabledHelpText": "Anonim kullanım ve hata bilgilerini {appName} sunucularına gönderin. Buna, tarayıcınız, hangi {appName} WebUI sayfalarını kullandığınız, hata raporlamanın yanı sıra işletim sistemi ve çalışma zamanı sürümü hakkındaki bilgiler de dahildir. Bu bilgiyi özelliklere ve hata düzeltmelerine öncelik vermek için kullanacağız.",
"Backup": "Yedek",
"Backup": "Yedekler",
"BindAddress": "Bind Adresi",
"DownloadClientFreeboxSettingsApiUrl": "API URL'si",
"DownloadClientFreeboxSettingsAppId": "Uygulama kimliği",
"DownloadClientFreeboxNotLoggedIn": "Giriş yapmadınız",
"DownloadClientFreeboxSettingsAppToken": "Uygulama Token'ı",
"DownloadClientFreeboxSettingsAppToken": "Uygulama Jetonu",
"DownloadClientFreeboxSettingsAppTokenHelpText": "Freebox API'sine erişim oluşturulurken alınan uygulama jetonu (ör. 'app_token')",
"Apply": "Uygula",
"DownloadClientFreeboxAuthenticationError": "Freebox API'sinde kimlik doğrulama başarısız oldu. Sebep: {errorDescription}",
@@ -276,7 +276,7 @@
"DownloadClientQbittorrentSettingsFirstAndLastFirst": "İlk ve Son İlk",
"DownloadClientQbittorrentSettingsContentLayoutHelpText": "qBittorrent'in yapılandırılmış içerik düzenini mi, torrentteki orijinal düzeni mi kullanacağınızı yoksa her zaman bir alt klasör oluşturup oluşturmayacağınızı (qBittorrent 4.3.2+)",
"DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName}, etiketi qBittorrent'e ekleyemedi.",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "Sıraya Alma Etkin Değil",
"DownloadClientQbittorrentValidationQueueingNotEnabled": "kuyruğa Alma Etkin Değil",
"DownloadClientRTorrentSettingsUrlPathHelpText": "XMLRPC uç noktasının yolu, bkz. {url}. RuTorrent kullanılırken bu genellikle RPC2 veya [ruTorrent yolu]{url2} olur.",
"DownloadClientSabnzbdValidationDevelopVersion": "Sabnzbd, sürüm 3.0.0 veya üzerini varsayarak sürüm geliştirir.",
"DownloadClientValidationUnableToConnect": "{clientName} ile bağlantı kurulamıyor",
@@ -313,7 +313,7 @@
"DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "İçe aktarma sorunlarını önlemek için {appName}'in kullandığı kategori için Tarih sıralamayı devre dışı bırakmalısınız. Düzeltmek için Sabnzbd'a gidin.",
"DownloadClientValidationGroupMissing": "Grup mevcut değil",
"DownloadClientValidationTestTorrents": "Torrentlerin listesi alınamadı: {exceptionMessage}",
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName}, Tamamlanan İndirme İşlemini yapılandırıldığı şekilde gerçekleştiremeyecek. Bunu qBittorrent'te (menüde 'Araçlar -> Seçenekler...') 'Seçenekler -> BitTorrent -> Paylaşım Oranı Sınırlaması'nı 'Kaldır' yerine 'Duraklat' olarak değiştirerek düzeltebilirsiniz.",
"DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName}, Tamamlanan İndirme İşlemini yapılandırıldığı şekilde gerçekleştiremeyecek. Bunu qBittorrent'te (menüde 'Araçlar -> Seçenekler...') 'Seçenekler -> BitTorrent -> Paylaşım Oranı Sınırlaması'nı 'Kaldır' yerine 'Duraklat' olarak değiştirerek düzeltebilirsiniz",
"DownloadClientSettingsCategoryHelpText": "{appName}'e özel bir kategori eklemek, {appName} dışındaki ilgisiz indirmelerle çakışmaları önler. Kategori kullanmak isteğe bağlıdır ancak önemle tavsiye edilir.",
"DownloadClientSettingsOlderPriority": "Eski Önceliği",
"DownloadClientValidationTestNzbs": "NZB'lerin listesi alınamadı: {exceptionMessage}",
@@ -337,7 +337,7 @@
"Imported": "İçe aktarıldı",
"NotificationsAppriseSettingsTagsHelpText": "İsteğe bağlı olarak yalnızca uygun şekilde etiketlenenleri bilgilendirin.",
"NotificationsDiscordSettingsAvatar": "Avatar",
"NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Tokenı",
"NotificationsGotifySettingsAppTokenHelpText": "Gotify tarafından oluşturulan Uygulama Jetonu",
"ImportScriptPath": "Komut Dosyası Yolunu İçe Aktar",
"History": "Geçmiş",
"EditSelectedImportLists": "Seçilen İçe Aktarma Listelerini Düzenle",
@@ -349,7 +349,7 @@
"FormatShortTimeSpanHours": "{hours} saat",
"FormatRuntimeMinutes": "{minutes}dk",
"FullColorEventsHelpText": "Etkinliğin tamamını yalnızca sol kenar yerine durum rengiyle renklendirecek şekilde stil değiştirildi. Gündem için geçerli değildir",
"GrabId": "ID Yakala",
"GrabId": "ID'den Yakala",
"ImportUsingScriptHelpText": "Bir komut dosyası kullanarak içe aktarmak için dosyaları kopyalayın (ör. kod dönüştürme için)",
"InstanceNameHelpText": "Sekmedeki örnek adı ve Syslog uygulaması adı için",
"ManageDownloadClients": "İndirme İstemcilerini Yönet",
@@ -373,8 +373,8 @@
"NotificationsKodiSettingsDisplayTimeHelpText": "Bildirimin ne kadar süreyle görüntüleneceği (Saniye cinsinden)",
"NotificationsMailgunSettingsUseEuEndpoint": "AB Uç Noktasını Kullan",
"NotificationsMailgunSettingsSenderDomain": "Gönderen Alanı",
"NotificationsNtfySettingsAccessToken": "Erişim Token'ı",
"NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı belirteç tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir",
"NotificationsNtfySettingsAccessToken": "Erişim Jetonu",
"NotificationsNtfySettingsAccessTokenHelpText": "İsteğe bağlı jeton tabanlı yetkilendirme. Kullanıcı adı/şifreye göre önceliklidir",
"NotificationsNtfySettingsPasswordHelpText": "İsteğe bağlı şifre",
"NotificationsNtfySettingsTagsEmojisHelpText": "Kullanılacak etiketlerin veya emojilerin isteğe bağlı listesi",
"NotificationsNtfySettingsTopics": "Konular",
@@ -393,8 +393,8 @@
"MustContainHelpText": "İzin bu şartlardan en az birini içermelidir (büyük/küçük harfe duyarlı değildir)",
"NotificationStatusAllClientHealthCheckMessage": "Arızalar nedeniyle tüm bildirimler kullanılamıyor",
"EditSelectedIndexers": "Seçili Dizin Oluşturucuları Düzenle",
"EnableProfileHelpText": "Sürüm profilini etkinleştirmek için işaretleyin",
"EnableRssHelpText": "{appName}, RSS Senkronizasyonu aracılığıyla düzenli aralıklarla sürüm değişikliği aradığında kullanacak",
"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}",
"FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}",
"NotificationsNtfySettingsTagsEmojis": "Ntfy Etiketler ve Emojiler",
@@ -425,7 +425,7 @@
"NotificationsJoinValidationInvalidDeviceId": "Cihaz kimlikleri geçersiz görünüyor.",
"NotificationsNtfySettingsClickUrl": "URL'ye tıklayın",
"NotificationsNotifiarrSettingsApiKeyHelpText": "Profilinizdeki API anahtarınız",
"EditReleaseProfile": "Sürüm Profilini Düzenle",
"EditReleaseProfile": "Yayımlama Profilini Düzenle",
"EditSelectedDownloadClients": "Seçilen İndirme İstemcilerini Düzenle",
"FormatShortTimeSpanMinutes": "{minutes} dakika",
"FullColorEvents": "Tam Renkli Etkinlikler",
@@ -482,7 +482,7 @@
"NotificationsEmailSettingsCcAddress": "CC Adres(ler)i",
"NotificationsEmailSettingsRecipientAddress": "Alıcı Adres(ler)i",
"NotificationsEmbySettingsUpdateLibraryHelpText": "İçe Aktarma, Yeniden Adlandırma veya Silme sırasında Kitaplık Güncellensin mi?",
"NotificationsGotifySettingsAppToken": "Uygulama Token'ı",
"NotificationsGotifySettingsAppToken": "Uygulama Jetonu",
"NotificationsJoinSettingsDeviceIds": "Cihaz Kimlikleri",
"NotificationsJoinSettingsNotificationPriority": "Bildirim Önceliği",
"Test": "Sına",
@@ -506,5 +506,291 @@
"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 dizin oluşturucudan 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)",
"NotificationsPlexSettingsAuthToken": "Kimlik Doğrulama Jetonu",
"NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Alıcının Grup Kimliği / Telefon Numarası",
"NotificationsSignalSettingsUsernameHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan kullanıcı adı",
"NotificationsTraktSettingsAuthenticateWithTrakt": "Trakt ile kimlik doğrulama",
"NotificationsTwitterSettingsDirectMessageHelpText": "Herkese açık mesaj yerine doğrudan mesaj gönderin",
"NotificationsValidationUnableToSendTestMessage": "Test mesajı gönderilemiyor: {exceptionMessage}",
"OnManualInteractionRequired": "Manuel Etkileşim Gerektiğinde",
"Release": "Yayın",
"Remove": "Kaldır",
"RemoveFromDownloadClient": "İndirme İstemcisinden Kaldır",
"RemoveFromDownloadClientHint": "İndirilenleri ve dosyaları indirme istemcisinden kaldırır",
"ResetTitles": "Başlıkları Sıfırla",
"Space": "Boşluk",
"UsenetDelayTime": "Usenet Gecikmesi: {usenetDelay}",
"BackupIntervalHelpText": "Otomatik yedeklemeler arasındaki zaman aralığı",
"BlocklistRelease": "Kara Liste Sürümü",
"CustomFormats": "Özel Formatlar",
"DeleteDownloadClientMessageText": "'{name}' indirme istemcisini silmek istediğinizden emin misiniz?",
"DeleteIndexerMessageText": "'{name}' dizinleyicisini silmek istediğinizden emin misiniz?",
"OneMinute": "1 dakika",
"TaskUserAgentTooltip": "API'yi çağıran uygulama tarafından sağlanan Kullanıcı Aracısı",
"SkipRedownload": "Yeniden İndirmeyi Atla",
"RestartLater": "Daha sonra yeniden başlayacağım",
"DeleteTagMessageText": "'{label}' etiketini silmek istediğinizden emin misiniz?",
"DeleteBackupMessageText": "'{name}' yedeğini silmek istediğinizden emin misiniz?",
"DeleteRemotePathMappingMessageText": "Bu uzak yol eşlemesini silmek istediğinizden emin misiniz?",
"QualitiesLoadError": "Nitelikler yüklenemiyor",
"SelectAll": "Hepsini Seç",
"SslCertPasswordHelpText": "Pfx dosyasının şifresi",
"SslCertPathHelpText": "Pfx dosyasının yolu",
"TorrentBlackholeSaveMagnetFilesReadOnly": "Sadece oku",
"NotificationsSlackSettingsChannelHelpText": "Gelen webhook için varsayılan kanalı geçersiz kılar (#diğer kanal)",
"NotificationsTelegramSettingsSendSilently": "Sessizce Gönder",
"UseSsl": "SSL kullan",
"NotificationsTelegramSettingsTopicIdHelpText": "Bu konuya bildirim göndermek için bir Konu Kimliği belirtin. Genel konuyu kullanmak için boş bırakın (Yalnızca Süper Gruplar)",
"NotificationsTwitterSettingsConsumerSecretHelpText": "Twitter uygulamasından kullanıcı gizliliği",
"VideoDynamicRange": "Video Dinamik Aralığı",
"WouldYouLikeToRestoreBackup": "'{name}' yedeğini geri yüklemek ister misiniz?",
"NotificationsTwitterSettingsDirectMessage": "Direk mesaj",
"PasswordConfirmation": "Şifre onayı",
"OrganizeRenamingDisabled": "Yeniden adlandırma devre dışı bırakıldı, yeniden adlandırılacak bir şey yok",
"ResetQualityDefinitionsMessageText": "Kalite tanımlarını sıfırlamak istediğinizden emin misiniz?",
"SelectFolderModalTitle": "{modalTitle} - Klasör seç",
"SelectReleaseGroup": "Yayımlama Grubunu Seçin",
"RemoveFailedDownloads": "Başarısız İndirmeleri Kaldır",
"Scheduled": "Planlı",
"Underscore": "Vurgula",
"SetIndexerFlags": "Dizin Oluşturucu Bayraklarını Ayarla",
"SetReleaseGroup": "Yayımlama Grubunu Ayarla",
"SetIndexerFlagsModalTitle": "{modalTitle} - Dizin Oluşturucu Bayraklarını Ayarla",
"SslCertPassword": "SSL Sertifika Şifresi",
"Rating": "Puan",
"GrabRelease": "Yayın Yakalama",
"NotificationsNtfyValidationAuthorizationRequired": "Yetkilendirme gerekli",
"NotificationsPushBulletSettingSenderId": "Gönderen ID",
"NotificationsPushBulletSettingsChannelTags": "Kanal Etiketleri",
"NotificationsPushBulletSettingsAccessToken": "Erişim Jetonu",
"NotificationsPushBulletSettingsChannelTagsHelpText": "Bildirimlerin gönderileceği Kanal Etiketleri Listesi",
"NotificationsPushBulletSettingsDeviceIdsHelpText": "Cihaz kimliklerinin listesi (tüm cihazlara göndermek için boş bırakın)",
"NotificationsPushcutSettingsNotificationName": "Bildirim Adı",
"NotificationsPushcutSettingsTimeSensitive": "Zamana duyarlı",
"NotificationsPushoverSettingsDevicesHelpText": "Cihaz adlarının listesi (tüm cihazlara göndermek için boş bırakın)",
"NotificationsPushoverSettingsRetryHelpText": "Acil durum uyarılarını yeniden deneme aralığı, minimum 30 saniye",
"NotificationsPushoverSettingsRetry": "Yeniden dene",
"NotificationsPushoverSettingsSoundHelpText": "Bildirim sesi, varsayılanı kullanmak için boş bırakın",
"NotificationsSettingsUpdateMapPathsFrom": "Harita Yolları",
"NotificationsSignalSettingsGroupIdPhoneNumber": "Grup Kimliği / Telefon Numarası",
"NotificationsSettingsWebhookUrl": "Webhook URL'si",
"NotificationsSignalSettingsSenderNumber": "Gönderen Numarası",
"NotificationsSignalSettingsSenderNumberHelpText": "Signal-api'deki gönderen kaydının telefon numarası",
"NotificationsSignalValidationSslRequired": "SSL gerekli görünüyor",
"NotificationsSimplepushSettingsEvent": "Etkinlik",
"NotificationsSlackSettingsIcon": "Simge",
"NotificationsSlackSettingsIconHelpText": "Slack'e gönderilen mesajlar için kullanılan simgeyi değiştirin (Emoji veya URL)",
"NotificationsSynologyValidationInvalidOs": "Bir Synology olmalı",
"NotificationsTelegramSettingsBotToken": "Bot Jetonu",
"NotificationsTelegramSettingsChatIdHelpText": "Mesaj almak için botla bir konuşma başlatmanız veya onu grubunuza eklemeniz gerekir",
"NotificationsTelegramSettingsTopicId": "Konu Kimliği",
"NotificationsTraktSettingsAuthUser": "Yetkilendirilmiş Kullanıcı",
"NotificationsTwitterSettingsConsumerKey": "Kullanıcı anahtarı",
"NotificationsTwitterSettingsConnectToTwitter": "Twitter / X'e bağlanın",
"NotificationsValidationInvalidAccessToken": "Erişim Jetonu geçersiz",
"NotificationsValidationInvalidAuthenticationToken": "Kimlik Doğrulama Jetonu geçersiz",
"OverrideAndAddToDownloadQueue": "Geçersiz kıl ve indirme kuyruğuna ekle",
"Parse": "Ayrıştır",
"PackageVersion": "Paket Versiyonu",
"PostImportCategory": "İçe Aktarma Sonrası Kategorisi",
"Queued": "Kuyruğa alındı",
"ReleaseHash": "Yayın Karması",
"RemoveTagsAutomatically": "Otomatik Etiketlemeyi Kaldır",
"RestartRequiredWindowsService": "{appName} hizmetini hangi kullanıcının çalıştırdığına bağlı olarak, hizmetin otomatik olarak başlamasından önce {appName} hizmetini yönetici olarak bir kez yeniden başlatmanız gerekebilir.",
"SelectLanguageModalTitle": "{modalTitle} - Dil Seç",
"SkipRedownloadHelpText": "{appName} uygulamasının bu öğe için alternatif bir yayın indirmeye çalışmasını engeller",
"SslCertPath": "SSL Sertifika Yolu",
"StopSelecting": "Düzenlemeden Çık",
"TableOptionsButton": "Tablo Seçenekleri Butonu",
"TheLogLevelDefault": "Günlük düzeyi varsayılan olarak 'Bilgi' şeklindedir ve [Genel Ayarlar](/settings/general) bölümünden değiştirilebilir",
"NotificationsTwitterSettingsAccessToken": "Erişim Jetonu",
"AutoRedownloadFailedHelpText": "Otomatik olarak farklı bir Yayın arayın ve indirmeye çalışın",
"Queue": "Sırada",
"RemoveFromQueue": "Kuyruktan kaldır",
"TorrentDelayTime": "Torrent Gecikmesi: {torrentDelay}",
"Yes": "Evet",
"ChmodFolderHelpText": "Sekizli, medya klasörlerine ve dosyalara içe aktarma / yeniden adlandırma sırasında uygulanır (yürütme bitleri olmadan)",
"DeleteNotificationMessageText": "'{name}' bildirimini silmek istediğinizden emin misiniz?",
"Or": "veya",
"OverrideGrabModalTitle": "Geçersiz Kıl ve Yakala - {title}",
"PreferProtocol": "{preferredProtocol}'u tercih edin",
"PreferredProtocol": "Tercih Edilen Protokol",
"PublishedDate": "Yayınlanma Tarihi",
"RemoveQueueItem": "Kaldır - {sourceTitle}",
"RemoveQueueItemConfirmation": "'{sourceTitle}' dosyasını kuyruktan kaldırmak istediğinizden emin misiniz?",
"RemoveSelectedBlocklistMessageText": "Seçilen öğeleri engellenenler listesinden kaldırmak istediğinizden emin misiniz?",
"RemoveSelectedItemQueueMessageText": "1 öğeyi kuyruktan kaldırmak istediğinizden emin misiniz?",
"RemoveSelectedItem": "Seçilen Öğeyi Kaldır",
"RemovedFromTaskQueue": "Görev kuyruğundan kaldırıldı",
"ResetAPIKeyMessageText": "API Anahtarınızı sıfırlamak istediğinizden emin misiniz?",
"ResetQualityDefinitions": "Kalite Tanımlarını Sıfırla",
"SelectLanguages": "Dil Seçin",
"StartupDirectory": "Başlangıç Dizini",
"TablePageSizeMaximum": "Sayfa boyutu {maximumValue} değerini aşmamalıdır",
"TablePageSizeHelpText": "Her sayfada gösterilecek öğe sayısı",
"TablePageSizeMinimum": "Sayfa boyutu en az {minimumValue} olmalıdır",
"TorrentBlackholeSaveMagnetFiles": "Magnet Dosyalarını Kaydet",
"TorrentBlackholeTorrentFolder": "Torrent Klasörü",
"TypeOfList": "{typeOfList} Liste",
"UnknownEventTooltip": "Bilinmeyen etkinlik",
"UpdateMechanismHelpText": "{appName}'ın yerleşik güncelleyicisini veya bir komut dosyasını kullanın",
"UsenetBlackhole": "Usenet Blackhole",
"CalendarOptions": "Takvim Seçenekleri",
"CustomFormatHelpText": "{appName}, özel formatlarla eşleşen puanların toplamını kullanarak her yayını puanlar. Yeni bir yayının, puanı aynı veya daha iyi kalitede iyileştirecekse, {appName} onu alacaktır.",
"CustomFormatsSettingsSummary": "Özel Formatlar ve Ayarlar",
"CustomFormatsSettings": "Özel Format Ayarları",
"NotificationsPushoverSettingsExpireHelpText": "Acil durum uyarılarını yeniden denemek için maksimum süre, maksimum 86400 saniye\"",
"NotificationsPushoverSettingsExpire": "Süresi dolmuş",
"NotificationsPushoverSettingsUserKey": "Kullanıcı Anahtarı",
"NotificationsSlackSettingsChannel": "Kanal",
"NotificationsValidationInvalidHttpCredentials": "HTTP Kimlik Doğrulama kimlik bilgileri geçersiz: {exceptionMessage}",
"OriginalLanguage": "Orjinal Dil",
"ReleaseProfiles": "Yayımlama Profilleri",
"SupportedAutoTaggingProperties": "{appName}, otomatik etiketleme kuralları için takip özelliklerini destekler",
"OptionalName": "İsteğe bağlı isim",
"OrganizeRelativePaths": "Tüm yollar şuna göredir: `{path}`",
"OverrideGrabNoQuality": "Kalite seçilmelidir",
"ParseModalErrorParsing": "Ayrıştırmada hata oluştu. Lütfen tekrar deneyin.",
"PackageVersionInfo": "{packageAuthor} tarafından {packageVersion}",
"RemoveCompleted": "Tamamlananları Kaldır",
"RemoveFailed": "Başarısızları Kaldır",
"ResetDefinitions": "Tanımları Sıfırla",
"RestartRequiredToApplyChanges": "{appName}, değişikliklerin uygulanabilmesi için yeniden başlatmayı gerektiriyor. Şimdi yeniden başlatmak istiyor musunuz?",
"RetryingDownloadOn": "{date} tarihinde, {time} itibarıyla indirme işlemi yeniden deneniyor",
"RssSyncIntervalHelpText": "Dakika cinsinden periyot. Devre dışı bırakmak için sıfıra ayarlayın (tüm otomatik yayın yakalamayı durduracaktır)",
"TorrentBlackhole": "Blackhole Torrent",
"PrioritySettings": "Öncelik: {priority}",
"SubtitleLanguages": "Altyazı Dilleri",
"OrganizeNothingToRename": "Başarılı! İşim bitti, yeniden adlandırılacak dosya yok.",
"MinimumCustomFormatScore": "Minimum Özel Format Puanı",
"MinimumAgeHelpText": "Yalnızca Usenet: NZB'lerin alınmadan önceki dakika cinsinden minimum yaşı. Yeni yayınların usenet sağlayıcınıza yayılması için zaman tanımak için bunu kullanın.",
"QueueLoadError": "Kuyruk yüklenemedi",
"SelectDropdown": "Seçimler...",
"NotificationsSettingsUpdateMapPathsTo": "Harita Yolları",
"NotificationsPlexSettingsAuthenticateWithPlexTv": "Plex.tv ile kimlik doğrulaması yapın",
"NotificationsSynologySettingsUpdateLibraryHelpText": "Bir kütüphane dosyasını güncellemek için localhost'ta synoindex'i çağırın",
"NotificationsSlackSettingsWebhookUrlHelpText": "Slack kanal webhook URL'si",
"Organize": "Düzenle",
"OnApplicationUpdate": "Uygulama Güncellemesinde",
"PendingDownloadClientUnavailable": "Beklemede - İndirme istemcisi kullanılamıyor",
"PreviewRename": "Yeniden Adlandır ve Önizle",
"LocalPath": "Yerel Yol",
"RemoveMultipleFromDownloadClientHint": "İndirilenleri ve dosyaları indirme istemcisinden kaldırır",
"RemoveTagsAutomaticallyHelpText": "Koşullar karşılanmazsa otomatik etiketlemeyi kaldırın",
"RemoveSelectedItemsQueueMessageText": "{selectedCount} öğeyi kuyruktan kaldırmak istediğinizden emin misiniz?",
"Umask": "Umask",
"CustomFormatsLoadError": "Özel Formatlar yüklenemiyor",
"DownloadWarning": "Uyarıyı indir: {warningMessage}",
"Formats": "Formatlar",
"NotificationsValidationUnableToConnectToService": "{serviceName} hizmetine bağlanılamıyor",
"OrganizeLoadError": "Önizlemeler yüklenirken hata oluştu",
"ParseModalHelpTextDetails": "{appName}, başlığı ayrıştırmaya ve size konuyla ilgili ayrıntıları göstermeye çalışacak",
"NegateHelpText": "İşaretlenirse, bu {implementationName} koşulu eşleşirse özel format uygulanmayacaktır.",
"ParseModalHelpText": "Yukarıdaki girişe bir yayın başlığı girin",
"Period": "Periyot",
"NotificationsPushoverSettingsSound": "Ses",
"NotificationsSettingsUseSslHelpText": "{serviceName} hizmetine HTTP yerine HTTPS üzerinden bağlanın",
"NotificationsTelegramSettingsIncludeAppName": "{appName}'i Başlığa dahil et",
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Farklı uygulamalardan gelen bildirimleri ayırt etmek için isteğe bağlı olarak mesaj başlığının önüne {appName} ekleyin",
"NotificationsValidationInvalidApiKey": "API Anahtarı geçersiz",
"NotificationsValidationInvalidUsernamePassword": "Geçersiz kullanıcı adı veya şifre",
"NotificationsValidationUnableToConnect": "Bağlantı kurulamıyor: {exceptionMessage}",
"NotificationsValidationUnableToConnectToApi": "{service} API'sine bağlanılamıyor. Sunucu bağlantısı başarısız oldu: ({responseCode}) {exceptionMessage}",
"OnHealthRestored": "Sağlığın İyileştirilmesi Hakkında",
"OverrideGrabNoLanguage": "En az bir dil seçilmelidir",
"ParseModalUnableToParse": "Sağlanan başlık ayrıştırılamadı, lütfen tekrar deneyin.",
"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.",
"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",
"SslPort": "SSL Bağlantı Noktası",
"True": "Aktif",
"WantMoreControlAddACustomFormat": "Hangi indirmelerin tercih edileceği konusunda daha fazla kontrole mi ihtiyacınız var? Bir [Özel Format](/settings/customformats) ekleyin",
"XmlRpcPath": "XML RPC Yolu",
"RootFolderPath": "Kök Klasör Yolu",
"CustomFormat": "Özel Format",
"NotificationsPushBulletSettingSenderIdHelpText": "Bildirimlerin gönderileceği cihaz kimliği, pushbullet.com'da cihazın URL'sinde Device_iden kullanın (kendinizden göndermek için boş bırakın)",
"OrganizeModalHeader": "Düzenle & Yeniden Adlandır",
"Rejections": "Reddedilenler",
"Uptime": "Çalışma süresi",
"RemotePath": "Uzak Yol",
"File": "Dosya",
"ReleaseProfileIndexerHelpText": "Profilin hangi dizin oluşturucuya uygulanacağını belirtin",
"TablePageSize": "Sayfa Boyutu",
"NotificationsSynologyValidationTestFailed": "Synology veya synoındex mevcut değil",
"NotificationsTwitterSettingsAccessTokenSecret": "Erişim Jetonu Gizliliği",
"NotificationsSimplepushSettingsKey": "Anahtar",
"NotificationsPushBulletSettingsDeviceIds": "Cihaz Kimlikleri",
"NotificationsSendGridSettingsApiKeyHelpText": "SendGrid tarafından oluşturulan API Anahtar",
"NotificationsSettingsUpdateLibrary": "Kitaplığı Güncelle",
"NotificationsSettingsWebhookMethod": "Yöntem",
"SizeLimit": "Boyut Limiti",
"Sort": "Sınıflandır",
"ManualImport": "Manuel İçe Aktar",
"ReleaseProfilesLoadError": "Yayımlama Profilleri yüklenemiyor",
"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",
"Started": "Başlatıldı",
"Size": "Boyut",
"SupportedCustomConditions": "{appName}, aşağıdaki yayın özelliklerine göre özel koşulları destekler.",
"TestParsing": "Ayrıştırma Testi",
"ThemeHelpText": "Uygulama Kullanıcı Arayüzü Temasını Değiştirin, 'Otomatik' Teması, Açık veya Koyu modu ayarlamak için İşletim Sistemi Temanızı kullanacaktır. Theme.Park'tan ilham alındı",
"TorrentBlackholeSaveMagnetFilesExtension": "Magnet Dosya Uzantısını Kaydet",
"Theme": "Tema",
"Unknown": "Bilinmeyen",
"ReleaseGroup": "Yayımlayan Grup",
"ReleaseGroupFootNote": "İsteğe bağlı olarak, üç nokta (`...`) dahil olmak üzere maksimum bayt sayısına kadar kesmeyi kontrol edin. Sondan (ör. `{Release Group:30}`) veya baştan (ör. `{Release Group:-30}`) kesmenin her ikisi de desteklenir.`).",
"RemoveCompletedDownloads": "Tamamlanan İndirmeleri Kaldır",
"TagDetails": "Etiket Ayrıntıları - {label}",
"RemoveDownloadsAlert": "Kaldırma ayarları, yukarıdaki tabloda bireysel İndirme İstemcisi ayarlarına taşınmıştır.",
"TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Magnet bağlantıları için kullanılacak uzantı, varsayılan olarak '.magnet'tir",
"TorrentBlackholeSaveMagnetFilesHelpText": ".torrent dosyası yoksa magnet bağlantısını kaydedin (yalnızca indirme istemcisi bir dosyaya kaydedilen magnetleri destekliyorsa kullanışlıdır)",
"RemoveQueueItemsRemovalMethodHelpTextWarning": "'İndirme İstemcisinden Kaldır', indirilenleri ve dosyaları indirme istemcisinden kaldıracaktır.",
"TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Bu, dosyaları taşımak yerine {appName}'e Kopyalama veya Sabit Bağlantı kurma talimatını verecektir (ayarlara/sistem yapılandırmasına bağlı olarak)",
"Interval": "Periyot",
"NotificationsTelegramSettingsSendSilentlyHelpText": "Mesajı sessizce gönderir. Kullanıcılar sessiz bir bildirim alacak",
"NotificationsTraktSettingsAccessToken": "Erişim Jetonu",
"NotificationsTraktSettingsExpires": "Süresi doluyor",
"NotificationsTraktSettingsRefreshToken": "Jetonu Yenile",
"NotificationsTwitterSettingsConsumerKeyHelpText": "Twitter uygulamasından kullanıcı anahtarı",
"NotificationsTelegramSettingsChatId": "Sohbet Kimliği",
"NotificationsTwitterSettingsConsumerSecret": "Kullanıcı Gizliliği",
"NotificationsValidationInvalidApiKeyExceptionMessage": "API Anahtarı geçersiz: {exceptionMessage}",
"UnknownDownloadState": "Bilinmeyen indirme durumu: {state}",
"UpdateFiltered": "Filtrelenenleri Güncelle",
"UsenetBlackholeNzbFolder": "Nzb Klasörü",
"UsenetDelayHelpText": "Usenet'ten bir yayın almadan önce beklemek için dakika cinsinden gecikme",
"UpdaterLogFiles": "Güncelleme Günlük Dosyaları",
"Usenet": "Usenet",
"Filters": "Filtreler",
"ImportListsSettingsSummary": "Başka bir {appName} örneğinden veya Trakt listelerinden içe aktarın ve liste hariç tutma işlemlerini yönetin",
"NotificationsPushcutSettingsApiKeyHelpText": "API Anahtarları, Pushcut uygulamasının Hesap görünümünden yönetilebilir",
"NotificationsPushcutSettingsNotificationNameHelpText": "Pushcut uygulamasının Bildirimler sekmesindeki bildirim adı",
"NotificationsPushcutSettingsTimeSensitiveHelpText": "Bildirimi \"Zamana Duyarlı\" olarak işaretlemek için etkinleştirin",
"NotificationsTwitterSettingsMention": "Bahset",
"NotificationsTwitterSettingsMentionHelpText": "Gönderilen tweetlerde bu kullanıcıdan bahsedin",
"NotificationsPushoverSettingsDevices": "Cihazlar",
"NotificationsSettingsUpdateMapPathsFromHelpText": "{appName} yolu, {serviceName} kitaplık yolu konumunu {appName}'dan farklı gördüğünde seri yollarını değiştirmek için kullanılır ('Kütüphaneyi Güncelle' gerektirir)",
"NotificationsSettingsWebhookMethodHelpText": "Web hizmetine göndermek için hangi HTTP yönteminin kullanılacağı",
"NotificationsValidationUnableToSendTestMessageApiResponse": "Test mesajı gönderilemiyor. API'den yanıt: {error}",
"NzbgetHistoryItemMessage": "PAR Durumu: {parStatus} - Paketten Çıkarma Durumu: {unpackStatus} - Taşıma Durumu: {moveStatus} - Komut Dosyası Durumu: {scriptStatus} - Silme Durumu: {deleteStatus} - İşaretleme Durumu: {markStatus}",
"NotificationsSignalSettingsPasswordHelpText": "Signal-api'ye yönelik istekleri doğrulamak için kullanılan şifre",
"NotificationsSimplepushSettingsEventHelpText": "Anlık bildirimlerin davranışını özelleştirme",
"NotificationsSlackSettingsUsernameHelpText": "Slack'e gönderilecek kullanıcı adı",
"QueueFilterHasNoItems": "Seçilen kuyruk filtresinde hiç öğe yok",
"ReleaseGroups": "Yayımlama Grupları",
"IncludeCustomFormatWhenRenamingHelpText": "{Custom Formats} yeniden adlandırma formatına dahil et",
"Logging": "Loglama",
"MinutesSixty": "60 Dakika: {sixty}",
"SelectDownloadClientModalTitle": "{modalTitle} - İndirme İstemcisini Seçin",
"Repack": "Yeniden paketle"
}

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

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

@@ -183,7 +183,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Single or multi episode releases with multiple titles, then season and episode numbers after the last title. (Title1 / Title2 / ... / S1E1-2 of 6)
new Regex(@"^((?<title>.*?)[ ._]\/[ ._])+\(?S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?([ ._]of[ ._]\d+)?\)?[ ._][\(\[]",
new Regex(@"^((?<title>.*?)[ ._]\/[ ._])+\(?S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?(?:[ ._]of[ ._](?<episodecount>\d{1,2}))?\)?[ ._][\(\[]",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Multi-episode with title (S01E99-100, S01E05-06)
@@ -1087,6 +1087,12 @@ namespace NzbDrone.Core.Parser
result.FullSeason = true;
}
}
if (episodeCaptures.Count == 2 && matchCollection[0].Groups["episodecount"].Success && episodeCaptures.Last().Value == matchCollection[0].Groups["episodecount"].Value)
{
result.EpisodeNumbers = Array.Empty<int>();
result.FullSeason = true;
}
}
var seasons = new List<int>();

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

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Update.History.Events;
@@ -18,13 +19,15 @@ namespace NzbDrone.Core.Update.History
{
private readonly IUpdateHistoryRepository _repository;
private readonly IEventAggregator _eventAggregator;
private readonly IConfigFileProvider _configFileProvider;
private readonly Logger _logger;
private Version _prevVersion;
public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, Logger logger)
public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator, IConfigFileProvider configFileProvider, Logger logger)
{
_repository = repository;
_eventAggregator = eventAggregator;
_configFileProvider = configFileProvider;
_logger = logger;
}
@@ -58,7 +61,7 @@ namespace NzbDrone.Core.Update.History
public void Handle(ApplicationStartedEvent message)
{
if (BuildInfo.Version.Major == 10)
if (BuildInfo.Version.Major == 10 || !_configFileProvider.LogDbEnabled)
{
// Don't save dev versions, they change constantly
return;

View File

@@ -29,7 +29,7 @@ namespace NzbDrone.Core.Update
{
var branch = _configFileProvider.Branch;
var version = BuildInfo.Version;
var prevVersion = _updateHistoryService.PreviouslyInstalled();
var prevVersion = _configFileProvider.LogDbEnabled ? _updateHistoryService.PreviouslyInstalled() : null;
return _updatePackageProvider.GetRecentUpdates(branch, version, prevVersion);
}
}

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;
@@ -95,6 +94,15 @@ namespace NzbDrone.Host
.AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(appMode);
if (config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true))
{
c.AddLogDatabase();
}
else
{
c.AddDummyLogDatabase();
}
})
.ConfigureServices(services =>
{
@@ -131,6 +139,7 @@ namespace NzbDrone.Host
var enableSsl = config.GetValue<bool?>($"Sonarr:Server:{nameof(ServerOptions.EnableSsl)}") ?? config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
var sslCertPath = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPath)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
var sslCertPassword = config.GetValue<string>($"Sonarr:Server:{nameof(ServerOptions.SslCertPassword)}") ?? config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
var logDbEnabled = config.GetValue<bool?>($"Sonarr:Log:{nameof(LogOptions.DbEnabled)}") ?? config.GetValue(nameof(ConfigFileProvider.LogDbEnabled), true);
var urls = new List<string> { BuildUrl("http", bindAddress, port) };
@@ -153,7 +162,14 @@ namespace NzbDrone.Host
.AddDatabase()
.AddStartupContext(context);
SchemaBuilder.Initialize(c);
if (logDbEnabled)
{
c.AddLogDatabase();
}
else
{
c.AddDummyLogDatabase();
}
})
.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;
@@ -56,7 +58,7 @@ namespace NzbDrone.Host
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
@@ -193,6 +195,7 @@ namespace NzbDrone.Host
}
public void Configure(IApplicationBuilder app,
IContainer container,
IStartupContext startupContext,
Lazy<IMainDatabase> mainDatabaseFactory,
Lazy<ILogDatabase> logDatabaseFactory,
@@ -220,9 +223,14 @@ namespace NzbDrone.Host
// instantiate the databases to initialize/migrate them
_ = mainDatabaseFactory.Value;
_ = logDatabaseFactory.Value;
dbTarget.Register();
if (configFileProvider.LogDbEnabled)
{
_ = logDatabaseFactory.Value;
dbTarget.Register();
}
SchemaBuilder.Initialize(container);
if (OsInfo.IsNotWindows)
{

View File

@@ -51,7 +51,7 @@ namespace Sonarr.Api.V3.AutoTagging
[RestPostById]
[Consumes("application/json")]
public ActionResult<AutoTaggingResource> Create(AutoTaggingResource autoTagResource)
public ActionResult<AutoTaggingResource> Create([FromBody] AutoTaggingResource autoTagResource)
{
var model = autoTagResource.ToModel(_specifications);
@@ -62,7 +62,7 @@ namespace Sonarr.Api.V3.AutoTagging
[RestPutById]
[Consumes("application/json")]
public ActionResult<AutoTaggingResource> Update(AutoTaggingResource resource)
public ActionResult<AutoTaggingResource> Update([FromBody] AutoTaggingResource resource)
{
var model = resource.ToModel(_specifications);

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

@@ -51,7 +51,7 @@ namespace Sonarr.Api.V3.Commands
[RestPostById]
[Consumes("application/json")]
[Produces("application/json")]
public ActionResult<CommandResource> StartCommand(CommandResource commandResource)
public ActionResult<CommandResource> StartCommand([FromBody] CommandResource commandResource)
{
var commandType =
_knownTypes.GetImplementations(typeof(Command))

View File

@@ -34,7 +34,7 @@ namespace Sonarr.Api.V3.Config
[RestPutById]
[Consumes("application/json")]
public virtual ActionResult<TResource> SaveConfig(TResource resource)
public virtual ActionResult<TResource> SaveConfig([FromBody] TResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)

View File

@@ -122,7 +122,7 @@ namespace Sonarr.Api.V3.Config
}
[RestPutById]
public ActionResult<HostConfigResource> SaveHostConfig(HostConfigResource resource)
public ActionResult<HostConfigResource> SaveHostConfig([FromBody] HostConfigResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)

View File

@@ -53,7 +53,7 @@ namespace Sonarr.Api.V3.Config
}
[RestPutById]
public ActionResult<NamingConfigResource> UpdateNamingConfig(NamingConfigResource resource)
public ActionResult<NamingConfigResource> UpdateNamingConfig([FromBody] NamingConfigResource resource)
{
var nameSpec = resource.ToModel();
ValidateFormatResult(nameSpec);

View File

@@ -32,7 +32,7 @@ namespace Sonarr.Api.V3.Config
}
[RestPutById]
public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource)
public override ActionResult<UiConfigResource> SaveConfig([FromBody] UiConfigResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)

View File

@@ -31,7 +31,7 @@ namespace Sonarr.Api.V3.CustomFilters
[RestPostById]
[Consumes("application/json")]
public ActionResult<CustomFilterResource> AddCustomFilter(CustomFilterResource resource)
public ActionResult<CustomFilterResource> AddCustomFilter([FromBody] CustomFilterResource resource)
{
var customFilter = _customFilterService.Add(resource.ToModel());
@@ -40,7 +40,7 @@ namespace Sonarr.Api.V3.CustomFilters
[RestPutById]
[Consumes("application/json")]
public ActionResult<CustomFilterResource> UpdateCustomFilter(CustomFilterResource resource)
public ActionResult<CustomFilterResource> UpdateCustomFilter([FromBody] CustomFilterResource resource)
{
_customFilterService.Update(resource.ToModel());
return Accepted(resource.Id);

View File

@@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.CustomFormats
[RestPostById]
[Consumes("application/json")]
public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource)
public ActionResult<CustomFormatResource> Create([FromBody] CustomFormatResource customFormatResource)
{
var model = customFormatResource.ToModel(_specifications);
@@ -60,7 +60,7 @@ namespace Sonarr.Api.V3.CustomFormats
[RestPutById]
[Consumes("application/json")]
public ActionResult<CustomFormatResource> Update(CustomFormatResource resource)
public ActionResult<CustomFormatResource> Update([FromBody] CustomFormatResource resource)
{
var model = resource.ToModel(_specifications);

View File

@@ -90,7 +90,7 @@ namespace Sonarr.Api.V3.EpisodeFiles
[RestPutById]
[Consumes("application/json")]
public ActionResult<EpisodeFileResource> SetQuality(EpisodeFileResource episodeFileResource)
public ActionResult<EpisodeFileResource> SetQuality([FromBody] EpisodeFileResource episodeFileResource)
{
var episodeFile = _mediaFileService.Get(episodeFileResource.Id);
episodeFile.Quality = episodeFileResource.Quality;

View File

@@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.ImportLists
[RestPostById]
[Consumes("application/json")]
public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportListExclusionResource resource)
public ActionResult<ImportListExclusionResource> AddImportListExclusion([FromBody] ImportListExclusionResource resource)
{
var importListExclusion = _importListExclusionService.Add(resource.ToModel());
@@ -58,7 +58,7 @@ namespace Sonarr.Api.V3.ImportLists
[RestPutById]
[Consumes("application/json")]
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion(ImportListExclusionResource resource)
public ActionResult<ImportListExclusionResource> UpdateImportListExclusion([FromBody] ImportListExclusionResource resource)
{
_importListExclusionService.Update(resource.ToModel());
return Accepted(resource.Id);

View File

@@ -68,7 +68,7 @@ namespace Sonarr.Api.V3.Indexers
[HttpPost]
[Consumes("application/json")]
public async Task<object> DownloadRelease(ReleaseResource release)
public async Task<object> DownloadRelease([FromBody] ReleaseResource release)
{
var remoteEpisode = _remoteEpisodeCache.Find(GetCacheKey(release));

View File

@@ -49,7 +49,7 @@ namespace Sonarr.Api.V3.Indexers
[HttpPost]
[Consumes("application/json")]
public ActionResult<List<ReleaseResource>> Create(ReleaseResource release)
public ActionResult<List<ReleaseResource>> Create([FromBody] ReleaseResource release)
{
_logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl);

View File

@@ -174,7 +174,8 @@ namespace Sonarr.Api.V3.Indexers
MagnetUrl = resource.MagnetUrl,
InfoHash = resource.InfoHash,
Seeders = resource.Seeders,
Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null
Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null,
IndexerFlags = (IndexerFlags)resource.IndexerFlags
};
}
else

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Instrumentation;
using Sonarr.Http;
using Sonarr.Http.Extensions;
@@ -10,16 +11,23 @@ namespace Sonarr.Api.V3.Logs
public class LogController : Controller
{
private readonly ILogService _logService;
private readonly IConfigFileProvider _configFileProvider;
public LogController(ILogService logService)
public LogController(ILogService logService, IConfigFileProvider configFileProvider)
{
_logService = logService;
_configFileProvider = configFileProvider;
}
[HttpGet]
[Produces("application/json")]
public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource paging, string level)
{
if (!_configFileProvider.LogDbEnabled)
{
return new PagingResource<LogResource>();
}
var pagingResource = new PagingResource<LogResource>(paging);
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>();

View File

@@ -35,7 +35,7 @@ namespace Sonarr.Api.V3.Profiles.Delay
[RestPostById]
[Consumes("application/json")]
public ActionResult<DelayProfileResource> Create(DelayProfileResource resource)
public ActionResult<DelayProfileResource> Create([FromBody] DelayProfileResource resource)
{
var model = resource.ToModel();
model = _delayProfileService.Add(model);
@@ -56,7 +56,7 @@ namespace Sonarr.Api.V3.Profiles.Delay
[RestPutById]
[Consumes("application/json")]
public ActionResult<DelayProfileResource> Update(DelayProfileResource resource)
public ActionResult<DelayProfileResource> Update([FromBody] DelayProfileResource resource)
{
var model = resource.ToModel();
_delayProfileService.Update(model);
@@ -76,7 +76,7 @@ namespace Sonarr.Api.V3.Profiles.Delay
}
[HttpPut("reorder/{id}")]
public List<DelayProfileResource> Reorder([FromRoute] int id, int? after)
public List<DelayProfileResource> Reorder([FromRoute] int id, [FromQuery] int? after)
{
ValidateId(id);

View File

@@ -15,7 +15,7 @@ namespace Sonarr.Api.V3.Profiles.Languages
[RestPostById]
[Produces("application/json")]
[Consumes("application/json")]
public ActionResult<LanguageProfileResource> Create(LanguageProfileResource resource)
public ActionResult<LanguageProfileResource> Create([FromBody] LanguageProfileResource resource)
{
return Accepted(resource);
}
@@ -28,7 +28,7 @@ namespace Sonarr.Api.V3.Profiles.Languages
[RestPutById]
[Produces("application/json")]
[Consumes("application/json")]
public ActionResult<LanguageProfileResource> Update(LanguageProfileResource resource)
public ActionResult<LanguageProfileResource> Update([FromBody] LanguageProfileResource resource)
{
return Accepted(resource);
}

View File

@@ -46,7 +46,7 @@ namespace Sonarr.Api.V3.Profiles.Quality
[RestPostById]
[Consumes("application/json")]
public ActionResult<QualityProfileResource> Create(QualityProfileResource resource)
public ActionResult<QualityProfileResource> Create([FromBody] QualityProfileResource resource)
{
var model = resource.ToModel();
model = _profileService.Add(model);
@@ -61,7 +61,7 @@ namespace Sonarr.Api.V3.Profiles.Quality
[RestPutById]
[Consumes("application/json")]
public ActionResult<QualityProfileResource> Update(QualityProfileResource resource)
public ActionResult<QualityProfileResource> Update([FromBody] QualityProfileResource resource)
{
var model = resource.ToModel();

View File

@@ -36,7 +36,7 @@ namespace Sonarr.Api.V3.Profiles.Release
}
[RestPostById]
public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource)
public ActionResult<ReleaseProfileResource> Create([FromBody] ReleaseProfileResource resource)
{
var model = resource.ToModel();
model = _profileService.Add(model);
@@ -50,7 +50,7 @@ namespace Sonarr.Api.V3.Profiles.Release
}
[RestPutById]
public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource)
public ActionResult<ReleaseProfileResource> Update([FromBody] ReleaseProfileResource resource)
{
var model = resource.ToModel();

View File

@@ -245,7 +245,7 @@ namespace Sonarr.Api.V3
[HttpPost("action/{name}")]
[Consumes("application/json")]
[Produces("application/json")]
public IActionResult RequestAction(string name, [FromBody] TProviderResource providerResource)
public IActionResult RequestAction([FromRoute] string name, [FromBody] TProviderResource providerResource)
{
var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null;
var providerDefinition = GetDefinition(providerResource, existingDefinition, false, false, false);

View File

@@ -23,7 +23,7 @@ namespace Sonarr.Api.V3.Qualities
}
[RestPutById]
public ActionResult<QualityDefinitionResource> Update(QualityDefinitionResource resource)
public ActionResult<QualityDefinitionResource> Update([FromBody] QualityDefinitionResource resource)
{
var model = resource.ToModel();
_qualityDefinitionService.Update(model);

View File

@@ -21,7 +21,7 @@ namespace Sonarr.Api.V3.Queue
}
[HttpPost("grab/{id:int}")]
public async Task<object> Grab(int id)
public async Task<object> Grab([FromRoute] int id)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);

View File

@@ -41,7 +41,7 @@ namespace Sonarr.Api.V3.RemotePathMappings
[RestPostById]
[Consumes("application/json")]
public ActionResult<RemotePathMappingResource> CreateMapping(RemotePathMappingResource resource)
public ActionResult<RemotePathMappingResource> CreateMapping([FromBody] RemotePathMappingResource resource)
{
var model = resource.ToModel();
@@ -62,7 +62,7 @@ namespace Sonarr.Api.V3.RemotePathMappings
}
[RestPutById]
public ActionResult<RemotePathMappingResource> UpdateMapping(RemotePathMappingResource resource)
public ActionResult<RemotePathMappingResource> UpdateMapping([FromBody] RemotePathMappingResource resource)
{
var mapping = resource.ToModel();

View File

@@ -50,7 +50,7 @@ namespace Sonarr.Api.V3.RootFolders
[RestPostById]
[Consumes("application/json")]
public ActionResult<RootFolderResource> CreateRootFolder(RootFolderResource rootFolderResource)
public ActionResult<RootFolderResource> CreateRootFolder([FromBody] RootFolderResource rootFolderResource)
{
var model = rootFolderResource.ToModel();

View File

@@ -19,7 +19,7 @@ namespace Sonarr.Api.V3.SeasonPass
[HttpPost]
[Consumes("application/json")]
public IActionResult UpdateAll(SeasonPassResource resource)
public IActionResult UpdateAll([FromBody] SeasonPassResource resource)
{
var seriesToUpdate = _seriesService.GetSeries(resource.Series.Select(s => s.Id));

View File

@@ -156,7 +156,7 @@ namespace Sonarr.Api.V3.Series
[RestPostById]
[Consumes("application/json")]
public ActionResult<SeriesResource> AddSeries(SeriesResource seriesResource)
public ActionResult<SeriesResource> AddSeries([FromBody] SeriesResource seriesResource)
{
var series = _addSeriesService.AddSeries(seriesResource.ToModel());
@@ -165,7 +165,7 @@ namespace Sonarr.Api.V3.Series
[RestPutById]
[Consumes("application/json")]
public ActionResult<SeriesResource> UpdateSeries(SeriesResource seriesResource, bool moveFiles = false)
public ActionResult<SeriesResource> UpdateSeries([FromBody] SeriesResource seriesResource, [FromQuery] bool moveFiles = false)
{
var series = _seriesService.GetSeries(seriesResource.Id);

View File

@@ -70,7 +70,7 @@ namespace Sonarr.Api.V3.System.Backup
}
[HttpPost("restore/{id:int}")]
public object Restore(int id)
public object Restore([FromRoute] int id)
{
var backup = GetBackup(id);

View File

@@ -36,14 +36,14 @@ namespace Sonarr.Api.V3.Tags
[RestPostById]
[Consumes("application/json")]
public ActionResult<TagResource> Create(TagResource resource)
public ActionResult<TagResource> Create([FromBody] TagResource resource)
{
return Created(_tagService.Add(resource.ToModel()).Id);
}
[RestPutById]
[Consumes("application/json")]
public ActionResult<TagResource> Update(TagResource resource)
public ActionResult<TagResource> Update([FromBody] TagResource resource)
{
_tagService.Update(resource.ToModel());
return Accepted(resource.Id);

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Update;
using NzbDrone.Core.Update.History;
using Sonarr.Http;
@@ -13,11 +14,13 @@ namespace Sonarr.Api.V3.Update
{
private readonly IRecentUpdateProvider _recentUpdateProvider;
private readonly IUpdateHistoryService _updateHistoryService;
private readonly IConfigFileProvider _configFileProvider;
public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService)
public UpdateController(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService, IConfigFileProvider configFileProvider)
{
_recentUpdateProvider = recentUpdateProvider;
_updateHistoryService = updateHistoryService;
_configFileProvider = configFileProvider;
}
[HttpGet]
@@ -45,7 +48,13 @@ namespace Sonarr.Api.V3.Update
installed.Installed = true;
}
var installDates = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate)
if (!_configFileProvider.LogDbEnabled)
{
return resources;
}
var updateHistory = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate);
var installDates = updateHistory
.DistinctBy(v => v.Version)
.ToDictionary(v => v.Version);

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

View File

@@ -21,7 +21,7 @@ namespace Sonarr.Http
public string SortKey { get; set; }
public SortDirection SortDirection { get; set; }
public int TotalRecords { get; set; }
public List<TResource> Records { get; set; }
public List<TResource> Records { get; set; } = new ();
public PagingResource()
{