mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
11 Commits
v4.0.4.161
...
v4.0.4.165
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d05214ae | ||
|
|
cc0a284660 | ||
|
|
f50a263f4f | ||
|
|
29176c8367 | ||
|
|
1eddf3a152 | ||
|
|
8360dd7a7b | ||
|
|
7e8d8500f2 | ||
|
|
cae134ec7b | ||
|
|
f81bb3ec19 | ||
|
|
128309068d | ||
|
|
73a4bdea52 |
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
54
frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx
Normal file
54
frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
37
frontend/src/App/ApplyTheme.tsx
Normal file
37
frontend/src/App/ApplyTheme.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
8
frontend/src/App/State/BlocklistAppState.ts
Normal file
8
frontend/src/App/State/BlocklistAppState.ts
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'customFormatScore': string;
|
||||
'customFormats': string;
|
||||
'languages': string;
|
||||
'quality': string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
16
frontend/src/typings/Blocklist.ts
Normal file
16
frontend/src/typings/Blocklist.ts
Normal 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;
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface UiSettings {
|
||||
theme: string;
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,8 +13,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission
|
||||
public bool IsFinished { get; set; }
|
||||
public long Eta { get; set; }
|
||||
public TransmissionTorrentStatus Status { get; set; }
|
||||
public int SecondsDownloading { get; set; }
|
||||
public int SecondsSeeding { get; set; }
|
||||
public long SecondsDownloading { get; set; }
|
||||
public long SecondsSeeding { get; set; }
|
||||
public string ErrorString { get; set; }
|
||||
public long DownloadedEver { get; set; }
|
||||
public long UploadedEver { get; set; }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"BlocklistAndSearch": "Blocklist and Search",
|
||||
"BlocklistAndSearchHint": "Start a search for a replacement after blocklisting",
|
||||
"BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting",
|
||||
"BlocklistFilterHasNoItems": "Selected blocklist filter contains no items",
|
||||
"BlocklistLoadError": "Unable to load blocklist",
|
||||
"BlocklistMultipleOnlyHint": "Blocklist without searching for replacements",
|
||||
"BlocklistOnly": "Blocklist Only",
|
||||
@@ -248,8 +249,8 @@
|
||||
"ConnectionLost": "Connection Lost",
|
||||
"ConnectionLostReconnect": "{appName} will try to connect automatically, or you can click reload below.",
|
||||
"ConnectionLostToBackend": "{appName} has lost its connection to the backend and will need to be reloaded to restore functionality.",
|
||||
"Connections": "Connections",
|
||||
"ConnectionSettingsUrlBaseHelpText": "Adds a prefix to the {connectionName} url, such as {url}",
|
||||
"Connections": "Connections",
|
||||
"Continuing": "Continuing",
|
||||
"ContinuingOnly": "Continuing Only",
|
||||
"ContinuingSeriesDescription": "More episodes/another season is expected",
|
||||
@@ -280,8 +281,8 @@
|
||||
"CustomFormats": "Custom Formats",
|
||||
"CustomFormatsLoadError": "Unable to load Custom Formats",
|
||||
"CustomFormatsSettings": "Custom Formats Settings",
|
||||
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
|
||||
"CustomFormatsSettingsSummary": "Custom Formats and Settings",
|
||||
"CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.",
|
||||
"CustomFormatsSpecificationFlag": "Flag",
|
||||
"CustomFormatsSpecificationLanguage": "Language",
|
||||
"CustomFormatsSpecificationMaximumSize": "Maximum Size",
|
||||
@@ -410,16 +411,16 @@
|
||||
"DownloadClientAriaSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Aria2 location",
|
||||
"DownloadClientCheckNoneAvailableHealthCheckMessage": "No download client is available",
|
||||
"DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Unable to communicate with {downloadClientName}. {errorMessage}",
|
||||
"DownloadClientDelugeSettingsDirectory": "Download Directory",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location",
|
||||
"DownloadClientDelugeSettingsUrlBaseHelpText": "Adds a prefix to the deluge json url, see {url}",
|
||||
"DownloadClientDelugeTorrentStateError": "Deluge is reporting an error",
|
||||
"DownloadClientDelugeValidationLabelPluginFailure": "Configuration of label failed",
|
||||
"DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} was unable to add the label to {clientName}.",
|
||||
"DownloadClientDelugeValidationLabelPluginInactive": "Label plugin not activated",
|
||||
"DownloadClientDelugeValidationLabelPluginInactiveDetail": "You must have the Label plugin enabled in {clientName} to use categories.",
|
||||
"DownloadClientDelugeSettingsDirectory": "Download Directory",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location",
|
||||
"DownloadClientDownloadStationProviderMessage": "{appName} is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account",
|
||||
"DownloadClientDownloadStationSettingsDirectoryHelpText": "Optional shared folder to put downloads into, leave blank to use the default Download Station location",
|
||||
"DownloadClientDownloadStationValidationApiVersion": "Download Station API version not supported, should be at least {requiredVersion}. It supports from {minVersion} to {maxVersion}",
|
||||
@@ -751,6 +752,7 @@
|
||||
"Group": "Group",
|
||||
"HardlinkCopyFiles": "Hardlink/Copy Files",
|
||||
"HasMissingSeason": "Has Missing Season",
|
||||
"HasUnmonitoredSeason": "Has Unmonitored Season",
|
||||
"Health": "Health",
|
||||
"HealthMessagesInfoBox": "You can find more information about the cause of these health check messages by clicking the wiki link (book icon) at the end of the row, or by checking your [logs]({link}). If you have difficulty interpreting these messages then you can reach out to our support, at the links below.",
|
||||
"Here": "here",
|
||||
@@ -867,12 +869,12 @@
|
||||
"ImportListsSimklSettingsUserListTypePlanToWatch": "Plan To Watch",
|
||||
"ImportListsSimklSettingsUserListTypeWatching": "Watching",
|
||||
"ImportListsSonarrSettingsApiKeyHelpText": "API Key of the {appName} instance to import from",
|
||||
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring",
|
||||
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored",
|
||||
"ImportListsSonarrSettingsFullUrl": "Full URL",
|
||||
"ImportListsSonarrSettingsFullUrlHelpText": "URL, including port, of the {appName} instance to import from",
|
||||
"ImportListsSonarrSettingsQualityProfilesHelpText": "Quality Profiles from the source instance to import from",
|
||||
"ImportListsSonarrSettingsRootFoldersHelpText": "Root Folders from the source instance to import from",
|
||||
"ImportListsSonarrSettingsSyncSeasonMonitoring": "Sync Season Monitoring",
|
||||
"ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sync season monitoring from {appName} instance, if enabled 'Monitor' will be ignored",
|
||||
"ImportListsSonarrSettingsTagsHelpText": "Tags from the source instance to import from",
|
||||
"ImportListsSonarrValidationInvalidUrl": "{appName} URL is invalid, are you missing a URL base?",
|
||||
"ImportListsTraktSettingsAdditionalParameters": "Additional Parameters",
|
||||
@@ -976,11 +978,11 @@
|
||||
"IndexerSettingsCookieHelpText": "If your site requires a login cookie to access the rss, you'll have to retrieve it via a browser.",
|
||||
"IndexerSettingsMinimumSeeders": "Minimum Seeders",
|
||||
"IndexerSettingsMinimumSeedersHelpText": "Minimum number of seeders required.",
|
||||
"IndexerSettingsMultiLanguageRelease": "Multi Languages",
|
||||
"IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?",
|
||||
"IndexerSettingsPasskey": "Passkey",
|
||||
"IndexerSettingsRejectBlocklistedTorrentHashes": "Reject Blocklisted Torrent Hashes While Grabbing",
|
||||
"IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "If a torrent is blocked by hash it may not properly be rejected during RSS/Search for some indexers, enabling this will allow it to be rejected after the torrent is grabbed, but before it is sent to the client.",
|
||||
"IndexerSettingsMultiLanguageRelease": "Multi Languages",
|
||||
"IndexerSettingsMultiLanguageReleaseHelpText": "What languages are normally in a multi release on this indexer?",
|
||||
"IndexerSettingsRssUrl": "RSS URL",
|
||||
"IndexerSettingsRssUrlHelpText": "Enter to URL to an {indexer} compatible RSS feed",
|
||||
"IndexerSettingsSeasonPackSeedTime": "Season-Pack Seed Time",
|
||||
@@ -1790,7 +1792,6 @@
|
||||
"SelectSeries": "Select Series",
|
||||
"SendAnonymousUsageData": "Send Anonymous Usage Data",
|
||||
"Series": "Series",
|
||||
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
|
||||
"SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them]({url}) .",
|
||||
"SeriesCannotBeFound": "Sorry, that series cannot be found.",
|
||||
"SeriesDetailsCountEpisodeFiles": "{episodeFileCount} episode files",
|
||||
@@ -1804,6 +1805,7 @@
|
||||
"SeriesFolderFormat": "Series Folder Format",
|
||||
"SeriesFolderFormatHelpText": "Used when adding a new series or moving series via the series editor",
|
||||
"SeriesFolderImportedTooltip": "Episode imported from series folder",
|
||||
"SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.",
|
||||
"SeriesID": "Series ID",
|
||||
"SeriesIndexFooterContinuing": "Continuing (All episodes downloaded)",
|
||||
"SeriesIndexFooterDownloading": "Downloading (One or more episodes)",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Common.Options;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore.Extensions;
|
||||
using Sonarr.Http.ClientSchema;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions;
|
||||
@@ -171,8 +170,6 @@ namespace NzbDrone.Host
|
||||
{
|
||||
c.AddDummyLogDatabase();
|
||||
}
|
||||
|
||||
SchemaBuilder.Initialize(c);
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using DryIoc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
@@ -26,6 +27,7 @@ using NzbDrone.SignalR;
|
||||
using Sonarr.Api.V3.System;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.Authentication;
|
||||
using Sonarr.Http.ClientSchema;
|
||||
using Sonarr.Http.ErrorManagement;
|
||||
using Sonarr.Http.Frontend;
|
||||
using Sonarr.Http.Middleware;
|
||||
@@ -193,6 +195,7 @@ namespace NzbDrone.Host
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app,
|
||||
IContainer container,
|
||||
IStartupContext startupContext,
|
||||
Lazy<IMainDatabase> mainDatabaseFactory,
|
||||
Lazy<ILogDatabase> logDatabaseFactory,
|
||||
@@ -227,6 +230,8 @@ namespace NzbDrone.Host
|
||||
dbTarget.Register();
|
||||
}
|
||||
|
||||
SchemaBuilder.Initialize(container);
|
||||
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Sonarr.Http.Frontend.Mappers
|
||||
return stream;
|
||||
}
|
||||
|
||||
protected string GetHtmlText()
|
||||
protected virtual string GetHtmlText()
|
||||
{
|
||||
if (RuntimeInfo.IsProduction && _generatedContent != null)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user