mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-06 13:31:28 -05:00
Compare commits
45 Commits
zeus
...
db-calls-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200be6451a | ||
|
|
b279984bd7 | ||
|
|
3f6f4fc65f | ||
|
|
3e5089719c | ||
|
|
ec69dfaabb | ||
|
|
aa13a40bad | ||
|
|
9b458812f1 | ||
|
|
1bdc48a889 | ||
|
|
e5d479a162 | ||
|
|
9a50fcb82a | ||
|
|
f2357e0b60 | ||
|
|
0591d05c3b | ||
|
|
299d50d56c | ||
|
|
7d3c01114b | ||
|
|
70376af70b | ||
|
|
9ef031bd9e | ||
|
|
3a9b276c43 | ||
|
|
aabf209a07 | ||
|
|
79c03f2fe6 | ||
|
|
9b36404071 | ||
|
|
ecfaea3885 | ||
|
|
bfbeb4c62e | ||
|
|
4b98d27f31 | ||
|
|
604d74270d | ||
|
|
15bb9139d1 | ||
|
|
32722eb704 | ||
|
|
e0c8a8f0d6 | ||
|
|
a3bb0541f0 | ||
|
|
e78bc34514 | ||
|
|
35c4538288 | ||
|
|
3981e816cd | ||
|
|
9354031571 | ||
|
|
a01328dc8c | ||
|
|
8cb6295ddc | ||
|
|
99f7d8bcf5 | ||
|
|
f13d479b88 | ||
|
|
23eb637bc3 | ||
|
|
3a786d0b9d | ||
|
|
6fb127235c | ||
|
|
5517e578b6 | ||
|
|
bced2e7b2e | ||
|
|
f7313369b5 | ||
|
|
b14e93e11f | ||
|
|
f5692d6cf1 | ||
|
|
a2d505c795 |
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.0.3'
|
||||
majorVersion: '5.1.2'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
@@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryFilterModal from './HistoryFilterModal';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
@@ -33,6 +34,7 @@ class History extends Component {
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
@@ -70,7 +72,8 @@ class History extends Component {
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={HistoryFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -144,8 +147,9 @@ History.propTypes = {
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
|
||||
@@ -11,11 +12,13 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.movies,
|
||||
(history, movies) => {
|
||||
createCustomFiltersSelector('history'),
|
||||
(history, movies, customFilters) => {
|
||||
return {
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
...history
|
||||
};
|
||||
}
|
||||
|
||||
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/Activity/History/HistoryFilterModal.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 { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,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 PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
@@ -21,6 +22,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 QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
@@ -153,11 +155,16 @@ class Queue extends Component {
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
count,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -220,6 +227,15 @@ class Queue extends Component {
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={QueueFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
@@ -241,7 +257,11 @@ class Queue extends Component {
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('QueueIsEmpty')}
|
||||
{
|
||||
selectedFilterKey !== 'all' && count > 0 ?
|
||||
translate('QueueFilterHasNoItems') :
|
||||
translate('QueueIsEmpty')
|
||||
}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
@@ -325,13 +345,22 @@ Queue.propTypes = {
|
||||
moviesError: 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,
|
||||
count: PropTypes.number.isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Queue.defaultProps = {
|
||||
count: 0
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
@@ -15,12 +16,16 @@ function createMapStateToProps() {
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
(state) => state.queue.status.item,
|
||||
createCustomFiltersSelector('queue'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
customFilters,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
@@ -106,6 +111,10 @@ class QueueConnector extends Component {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setQueueFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
@@ -140,6 +149,7 @@ class QueueConnector extends Component {
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
@@ -162,6 +172,7 @@ QueueConnector.propTypes = {
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueFilter: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
|
||||
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
54
frontend/src/Activity/Queue/QueueFilterModal.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 { setQueueFilter } from 'Store/Actions/queueActions';
|
||||
|
||||
function createQueueSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface QueueFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||
const sectionItems = useSelector(createQueueSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'queue';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setQueueFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
@@ -46,6 +47,7 @@ export interface CustomFilter {
|
||||
interface AppState {
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Movie from 'Movie/Movie';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
interface CalendarAppState extends AppSectionState<Movie> {
|
||||
filterBuilderProps: FilterBuilderProp<Movie>[];
|
||||
}
|
||||
interface CalendarAppState
|
||||
extends AppSectionState<Movie>,
|
||||
AppSectionFilterState<Movie> {}
|
||||
|
||||
export default CalendarAppState;
|
||||
|
||||
10
frontend/src/App/State/HistoryAppState.ts
Normal file
10
frontend/src/App/State/HistoryAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {}
|
||||
|
||||
export default HistoryAppState;
|
||||
@@ -1,6 +1,8 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
|
||||
type MovieCollectionAppState = AppSectionState<MovieCollection>;
|
||||
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||
itemMap: Record<number, number>;
|
||||
}
|
||||
|
||||
export default MovieCollectionAppState;
|
||||
|
||||
@@ -2,7 +2,11 @@ import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
AppSectionItemState,
|
||||
Error,
|
||||
} from './AppSectionState';
|
||||
|
||||
export interface StatusMessage {
|
||||
title: string;
|
||||
@@ -35,7 +39,9 @@ export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
||||
export interface QueuePagedAppState
|
||||
extends AppSectionState<Queue>,
|
||||
AppSectionFilterState<Queue> {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
|
||||
@@ -42,9 +42,9 @@ function Agenda(props) {
|
||||
<div className={styles.agenda}>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
const momentDate = moment(item.inCinemas);
|
||||
const momentDate = moment(item.sortDate);
|
||||
const showDate = index === 0 ||
|
||||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
|
||||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
||||
|
||||
return (
|
||||
<AgendaEventConnector
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.event {
|
||||
.overlay {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -111,5 +111,4 @@
|
||||
.releaseIcon {
|
||||
margin-right: 20px;
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class AgendaEvent extends Component {
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.date}>
|
||||
{(showDate) ? startTime.format(longDateFormat) : null}
|
||||
{showDate ? startTime.format(longDateFormat) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.releaseIcon}>
|
||||
|
||||
@@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() {
|
||||
);
|
||||
}
|
||||
|
||||
interface SeriesIndexFilterModalProps {
|
||||
interface CalendarFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function CalendarFilterModal(
|
||||
props: SeriesIndexFilterModalProps
|
||||
) {
|
||||
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||
const sectionItems = useSelector(createCalendarSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'calendar';
|
||||
|
||||
@@ -21,6 +21,7 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
...collection,
|
||||
movies: [...collection.movies].sort((a, b) => b.year - a.year),
|
||||
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class CollectionMovie extends Component {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
overview,
|
||||
year,
|
||||
tmdbId,
|
||||
@@ -123,11 +124,11 @@ class CollectionMovie extends Component {
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.overlayTitle}>
|
||||
{title}
|
||||
{title} {year > 0 ? `(${year})` : ''}
|
||||
</div>
|
||||
|
||||
{
|
||||
id &&
|
||||
id ?
|
||||
<div className={styles.overlayStatus}>
|
||||
<MovieIndexProgressBar
|
||||
monitored={monitored}
|
||||
@@ -138,7 +139,8 @@ class CollectionMovie extends Component {
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
isAvailable={isAvailable}
|
||||
/>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -171,6 +173,7 @@ CollectionMovie.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool,
|
||||
collectionId: PropTypes.number.isRequired,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
margin: 2px 4px;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
padding: 0 4px;
|
||||
border-left: 4px;
|
||||
border-left-style: solid;
|
||||
background-color: var(--white);
|
||||
background-color: var(--themeLightColor);
|
||||
color: var(--defaultColor);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class CollectionMovieLabel extends Component {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
year,
|
||||
status,
|
||||
monitored,
|
||||
isAvailable,
|
||||
@@ -35,9 +36,7 @@ class CollectionMovieLabel extends Component {
|
||||
}
|
||||
|
||||
<span>
|
||||
{
|
||||
title
|
||||
}
|
||||
{title} {year > 0 ? `(${year})` : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +61,7 @@ class CollectionMovieLabel extends Component {
|
||||
CollectionMovieLabel.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
status: PropTypes.string,
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool,
|
||||
|
||||
@@ -28,7 +28,6 @@ function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||
}
|
||||
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||
|
||||
const heights = [
|
||||
overviewOptions.showPosters ? posterHeight : 75,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
@@ -122,8 +121,8 @@ class CollectionOverviews extends Component {
|
||||
overviewOptions
|
||||
} = this.props;
|
||||
|
||||
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const posterWidth = overviewOptions.showPosters ? calculatePosterWidth(overviewOptions.size, isSmallScreen) : 0;
|
||||
const posterHeight = overviewOptions.showPosters ? calculatePosterHeight(posterWidth) : 0;
|
||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||
|
||||
this.setState({
|
||||
|
||||
@@ -6,9 +6,12 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
|
||||
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
@@ -58,9 +61,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.LANGUAGE:
|
||||
return LanguageFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
@@ -70,6 +79,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.MOVIE:
|
||||
return MovieFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.RELEASE_STATUS:
|
||||
return ReleaseStatusFilterBuilderRowValue;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
|
||||
interface FilterBuilderRowOnChangeProps {
|
||||
name: string;
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
interface FilterBuilderRowValueProps {
|
||||
filterType?: string;
|
||||
filterValue: string | number | object | string[] | number[] | object[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||
sectionItem: unknown[];
|
||||
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueProps;
|
||||
@@ -1,5 +1,5 @@
|
||||
.tag {
|
||||
height: 21px;
|
||||
display: flex;
|
||||
|
||||
&.isLastTag {
|
||||
.or {
|
||||
@@ -18,4 +18,5 @@
|
||||
.or {
|
||||
margin: 0 3px;
|
||||
color: var(--themeDarkColor);
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './FilterBuilderRowValueTag.css';
|
||||
|
||||
function FilterBuilderRowValueTag(props) {
|
||||
return (
|
||||
<span
|
||||
<div
|
||||
className={styles.tag}
|
||||
>
|
||||
<TagInputTag
|
||||
@@ -22,7 +22,7 @@ function FilterBuilderRowValueTag(props) {
|
||||
{translate('Or')}
|
||||
</div>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: 1,
|
||||
get name() {
|
||||
return translate('Grabbed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
get name() {
|
||||
return translate('Imported');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
get name() {
|
||||
return translate('Failed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
get name() {
|
||||
return translate('Deleted');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
get name() {
|
||||
return translate('Renamed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
get name() {
|
||||
return translate('Ignored');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||
}
|
||||
|
||||
export default HistoryEventTypeFilterBuilderRowValue;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const { items } = useSelector(createLanguagesSelector());
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||
}
|
||||
|
||||
export default LanguageFilterBuilderRowValue;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Movie from 'Movie/Movie';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
|
||||
|
||||
const tagList = allMovies
|
||||
.map((movie) => ({ id: movie.id, name: movie.title }))
|
||||
.sort(sortByName);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default MovieFilterBuilderRowValue;
|
||||
@@ -2,8 +2,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: $formLabelRightMarginWidth;
|
||||
padding-top: 8px;
|
||||
min-height: 35px;
|
||||
text-align: end;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
@@ -37,6 +37,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||
return inputTypes.OAUTH;
|
||||
case 'rootFolder':
|
||||
return inputTypes.ROOT_FOLDER_SELECT;
|
||||
case 'qualityProfile':
|
||||
return inputTypes.QUALITY_PROFILE_SELECT;
|
||||
default:
|
||||
return inputTypes.TEXT;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ModalContent.css';
|
||||
|
||||
function ModalContent(props) {
|
||||
@@ -28,6 +29,7 @@ function ModalContent(props) {
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
title={translate('Close')}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class PageHeader extends Component {
|
||||
aria-label="Donate"
|
||||
to="https://radarr.video/donate"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.translate}
|
||||
|
||||
@@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ export const BOOL = 'bool';
|
||||
export const BYTES = 'bytes';
|
||||
export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||
export const INDEXER = 'indexer';
|
||||
export const LANGUAGE = 'language';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const QUALITY = 'quality';
|
||||
export const QUALITY_PROFILE = 'qualityProfile';
|
||||
export const MOVIE = 'movie';
|
||||
export const RELEASE_STATUS = 'releaseStatus';
|
||||
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
||||
export const TAG = 'tag';
|
||||
|
||||
@@ -202,6 +202,12 @@
|
||||
.headerContent {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
|
||||
@@ -44,7 +44,7 @@ import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import MovieReleaseDatesConnector from './MovieReleaseDatesConnector';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
import MovieStatusLabel from './MovieStatusLabel';
|
||||
import MovieTagsConnector from './MovieTagsConnector';
|
||||
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
||||
@@ -433,7 +433,7 @@ class MovieDetails extends Component {
|
||||
}
|
||||
title={translate('ReleaseDates')}
|
||||
body={
|
||||
<MovieReleaseDatesConnector
|
||||
<MovieReleaseDates
|
||||
inCinemas={inCinemas}
|
||||
physicalRelease={physicalRelease}
|
||||
digitalRelease={digitalRelease}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import styles from './MovieReleaseDates.css';
|
||||
|
||||
function MovieReleaseDates(props) {
|
||||
const {
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
inCinemas,
|
||||
physicalRelease,
|
||||
digitalRelease
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
!!inCinemas &&
|
||||
<div >
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={icons.IN_CINEMAS}
|
||||
/>
|
||||
</div>
|
||||
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!!digitalRelease &&
|
||||
<div >
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={icons.MOVIE_FILE}
|
||||
/>
|
||||
</div>
|
||||
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!!physicalRelease &&
|
||||
<div >
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={icons.DISC}
|
||||
/>
|
||||
</div>
|
||||
{getRelativeDate(physicalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieReleaseDates.propTypes = {
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
physicalRelease: PropTypes.string,
|
||||
digitalRelease: PropTypes.string
|
||||
};
|
||||
|
||||
export default MovieReleaseDates;
|
||||
64
frontend/src/Movie/Details/MovieReleaseDates.tsx
Normal file
64
frontend/src/Movie/Details/MovieReleaseDates.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieReleaseDates.css';
|
||||
|
||||
interface MovieReleaseDatesProps {
|
||||
inCinemas: string;
|
||||
physicalRelease: string;
|
||||
digitalRelease: string;
|
||||
}
|
||||
|
||||
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||
const { inCinemas, physicalRelease, digitalRelease } = props;
|
||||
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inCinemas ? (
|
||||
<div title={translate('InCinemas')}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.IN_CINEMAS} />
|
||||
</div>
|
||||
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: false,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{digitalRelease ? (
|
||||
<div title={translate('DigitalRelease')}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.MOVIE_FILE} />
|
||||
</div>
|
||||
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: false,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{physicalRelease ? (
|
||||
<div title={translate('PhysicalRelease')}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.DISC} />
|
||||
</div>
|
||||
{getRelativeDate(
|
||||
physicalRelease,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{ timeFormat, timeForToday: false }
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieReleaseDates;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, null)(MovieReleaseDates);
|
||||
@@ -100,6 +100,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
||||
{translate('DigitalRelease')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="releaseDate"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('ReleaseDates')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="tmdbRating"
|
||||
sortKey={sortKey}
|
||||
|
||||
@@ -80,8 +80,12 @@ function DownloadClientOptions(props) {
|
||||
legend={translate('FailedDownloadHandling')}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Redownload')}</FormLabel>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
@@ -91,7 +95,28 @@ function DownloadClientOptions(props) {
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
settings.autoRedownloadFailed.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailedFromInteractiveSearch"
|
||||
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</Form>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('RemoveDownloadsAlert')}
|
||||
</Alert>
|
||||
|
||||
@@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
|
||||
import { set, updateServerSideCollection } from '../baseActions';
|
||||
|
||||
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
||||
const [baseSection] = section.split('.');
|
||||
|
||||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
@@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
||||
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters
|
||||
filters
|
||||
} = sectionState;
|
||||
|
||||
const customFilters = getState().customFilters.items.filter((customFilter) => {
|
||||
return customFilter.type === section || customFilter.type === baseSection;
|
||||
});
|
||||
|
||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||
|
||||
selectedFilters.forEach((filter) => {
|
||||
@@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url,
|
||||
data
|
||||
data,
|
||||
traditional: true
|
||||
}).request;
|
||||
|
||||
promise.done((response) => {
|
||||
|
||||
@@ -49,8 +49,6 @@ export const defaultState = {
|
||||
|
||||
selectedFilterKey: 'monitored',
|
||||
|
||||
customFilters: [],
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createAction } from 'redux-actions';
|
||||
import Icon from 'Components/Icon';
|
||||
import { filterTypes, icons, sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
@@ -177,6 +177,33 @@ export const defaultState = {
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: () => translate('EventType'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
|
||||
},
|
||||
{
|
||||
name: 'movieIds',
|
||||
label: () => translate('Movie'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.MOVIE
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.QUALITY
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
type: filterBuilderTypes.CONTAINS,
|
||||
valueType: filterBuilderValueTypes.LANGUAGE
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
|
||||
@@ -240,16 +241,55 @@ export const sortPredicates = {
|
||||
return item.year || undefined;
|
||||
},
|
||||
|
||||
inCinemas: function(item) {
|
||||
return item.inCinemas || '';
|
||||
inCinemas: function(item, direction) {
|
||||
if (item.inCinemas) {
|
||||
return moment(item.inCinemas).unix();
|
||||
}
|
||||
|
||||
if (direction === sortDirections.DESCENDING) {
|
||||
return -1 * Number.MAX_VALUE;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
},
|
||||
|
||||
physicalRelease: function(item) {
|
||||
return item.physicalRelease || '';
|
||||
physicalRelease: function(item, direction) {
|
||||
if (item.physicalRelease) {
|
||||
return moment(item.physicalRelease).unix();
|
||||
}
|
||||
|
||||
if (direction === sortDirections.DESCENDING) {
|
||||
return -1 * Number.MAX_VALUE;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
},
|
||||
|
||||
digitalRelease: function(item) {
|
||||
return item.digitalRelease || '';
|
||||
digitalRelease: function(item, direction) {
|
||||
if (item.digitalRelease) {
|
||||
return moment(item.digitalRelease).unix();
|
||||
}
|
||||
|
||||
if (direction === sortDirections.DESCENDING) {
|
||||
return -1 * Number.MAX_VALUE;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
},
|
||||
|
||||
releaseDate: function(item, direction) {
|
||||
const { inCinemas, digitalRelease, physicalRelease } = item;
|
||||
const releaseDate = digitalRelease || physicalRelease || inCinemas;
|
||||
|
||||
if (releaseDate) {
|
||||
return moment(releaseDate).unix();
|
||||
}
|
||||
|
||||
if (direction === sortDirections.DESCENDING) {
|
||||
return -1 * Number.MAX_VALUE;
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
@@ -159,6 +159,43 @@ export const defaultState = {
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
],
|
||||
|
||||
selectedFilterKey: 'all',
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: 'All',
|
||||
filters: []
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'movieIds',
|
||||
label: () => translate('Movie'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.MOVIE
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.QUALITY
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
type: filterBuilderTypes.CONTAINS,
|
||||
valueType: filterBuilderValueTypes.LANGUAGE
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Protocol'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.PROTOCOL
|
||||
}
|
||||
]
|
||||
},
|
||||
sortPredicates: {
|
||||
@@ -173,7 +210,8 @@ export const persistState = [
|
||||
'queue.paged.pageSize',
|
||||
'queue.paged.sortKey',
|
||||
'queue.paged.sortDirection',
|
||||
'queue.paged.columns'
|
||||
'queue.paged.columns',
|
||||
'queue.paged.selectedFilterKey'
|
||||
];
|
||||
|
||||
//
|
||||
@@ -198,6 +236,7 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage';
|
||||
export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage';
|
||||
export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage';
|
||||
export const SET_QUEUE_SORT = 'queue/setQueueSort';
|
||||
export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
|
||||
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
|
||||
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
|
||||
export const CLEAR_QUEUE = 'queue/clearQueue';
|
||||
@@ -222,6 +261,7 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE);
|
||||
export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE);
|
||||
export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
|
||||
export const setQueueSort = createThunk(SET_QUEUE_SORT);
|
||||
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
|
||||
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
|
||||
export const setQueueOption = createAction(SET_QUEUE_OPTION);
|
||||
export const clearQueue = createAction(CLEAR_QUEUE);
|
||||
@@ -268,7 +308,8 @@ export const actionHandlers = handleThunks({
|
||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
|
||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
|
||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
|
||||
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT
|
||||
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT,
|
||||
[serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER
|
||||
},
|
||||
fetchDataAugmenter
|
||||
),
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
function createCollectionSelector() {
|
||||
return createSelector(
|
||||
(state, { collectionId }) => collectionId,
|
||||
(state) => state.movieCollections.itemMap,
|
||||
(state) => state.movieCollections.items,
|
||||
(collectionId, itemMap, allCollections) => {
|
||||
if (allCollections && itemMap && collectionId in itemMap) {
|
||||
return allCollections[itemMap[collectionId]];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createCollectionSelector;
|
||||
17
frontend/src/Store/Selectors/createCollectionSelector.ts
Normal file
17
frontend/src/Store/Selectors/createCollectionSelector.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createCollectionSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { collectionId }: { collectionId: number }) => collectionId,
|
||||
(state: AppState) => state.movieCollections.itemMap,
|
||||
(state: AppState) => state.movieCollections.items,
|
||||
(collectionId, itemMap, allCollections) => {
|
||||
return allCollections && itemMap && collectionId in itemMap
|
||||
? allCollections[itemMap[collectionId]]
|
||||
: undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createCollectionSelector;
|
||||
@@ -72,6 +72,7 @@ function getInternalLink(source) {
|
||||
function getTestLink(source, props) {
|
||||
switch (source) {
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
|
||||
27
frontend/src/typings/History.ts
Normal file
27
frontend/src/typings/History.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from './CustomFormat';
|
||||
|
||||
export type HistoryEventType =
|
||||
| 'grabbed'
|
||||
| 'downloadFolderImported'
|
||||
| 'downloadFailed'
|
||||
| 'movieFileDeleted'
|
||||
| 'movieFolderImported'
|
||||
| 'movieFileRenamed'
|
||||
| 'downloadIgnored';
|
||||
|
||||
export default interface History {
|
||||
movieId: number;
|
||||
sourceTitle: string;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
qualityCutoffNotMet: boolean;
|
||||
date: string;
|
||||
downloadId: string;
|
||||
eventType: HistoryEventType;
|
||||
data: unknown;
|
||||
id: number;
|
||||
}
|
||||
@@ -73,15 +73,15 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"[Info] MigrationController: *** Migrating Database=radarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;token=mySecret;Enlist=False&username=mySecret;mypassword=mySecret;mypass=shouldkeep1;test_token=mySecret;password=123%@%_@!#^#@;use_password=mySecret;get_token=shouldkeep2;usetoken=shouldkeep3;passwrd=mySecret;")]
|
||||
|
||||
// Announce URLs (passkeys) Magnet & Tracker
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210imaveql2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210imaveql2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2ftracker.php%2f9pr04sg601233210IMAveQL2tyu8xyui%2fannounce""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce%2f9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2fannounce.php%3fpasskey%3d9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/tracker.php/9pr04sg601233210IMAveQL2tyu8xyui/announce""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce/9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""https://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui""}")]
|
||||
[TestCase(@"tracker"":""http://xxx.yyy/announce.php?passkey=9pr04sg601233210IMAveQL2tyu8xyui"",""info"":""http://xxx.yyy/info?a=b""")]
|
||||
|
||||
// Notifiarr
|
||||
[TestCase(@"https://xxx.yyy/api/v1/notification/radarr/9pr04sg6-0123-3210-imav-eql2tyu8xyui")]
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce"),
|
||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Path
|
||||
new (@"C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.8" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.118-22" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.1" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
|
||||
@@ -72,11 +72,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("How the Earth Was Made S02 Disc 1 1080i Blu-ray DTS-HD MA 2.0 AVC-TrollHD")]
|
||||
[TestCase("The Universe S03 Disc 1 1080p Blu-ray LPCM 2.0 AVC-TrollHD")]
|
||||
[TestCase("HELL ON WHEELS S02 1080P FULL BLURAY AVC DTS-HD MA 5 1")]
|
||||
[TestCase("Game.of.Thrones.S06.2016.DISC.3.BluRay.1080p.AVC.Atmos.TrueHD7.1-MTeam")]
|
||||
[TestCase("Game of Thrones S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")]
|
||||
[TestCase("Series Title S02 Disc 1 1080i Blu-ray DTS-HD MA 2.0 AVC-TrollHD")]
|
||||
[TestCase("Series Title S03 Disc 1 1080p Blu-ray LPCM 2.0 AVC-TrollHD")]
|
||||
[TestCase("SERIES TITLE S02 1080P FULL BLURAY AVC DTS-HD MA 5 1")]
|
||||
[TestCase("Series.Title.S06.2016.DISC.3.BluRay.1080p.AVC.Atmos.TrueHD7.1-MTeam")]
|
||||
[TestCase("Series Title S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")]
|
||||
[TestCase("Series Title S05 Disc 1 BluRay 1080p AVC Atmos TrueHD 7 1-MTeam")]
|
||||
[TestCase("Someone.the.Entertainer.Presents.S01.NTSC.3xDVD9.MPEG-2.DD2.0")]
|
||||
[TestCase("Series.Title.S00.The.Christmas.Special.2011.PAL.DVD5.DD2.0")]
|
||||
[TestCase("Series.of.Desire.2000.S1_D01.NTSC.DVD5")]
|
||||
public void should_return_false_if_matches_disc_format(string title)
|
||||
{
|
||||
_remoteMovie.Release.Title = title;
|
||||
|
||||
@@ -7,8 +7,6 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests
|
||||
@@ -36,10 +34,6 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
_listMovies = Builder<ImportListMovie>.CreateListOfSize(5)
|
||||
.Build().ToList();
|
||||
|
||||
Mocker.GetMock<ISearchForNewMovie>()
|
||||
.Setup(v => v.MapMovieToTmdbMovie(It.IsAny<MovieMetadata>()))
|
||||
.Returns<MovieMetadata>(m => new MovieMetadata { TmdbId = m.TmdbId });
|
||||
}
|
||||
|
||||
private void GivenList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult)
|
||||
@@ -135,9 +129,6 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeFalse();
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Verify(v => v.SyncMoviesForList(It.IsAny<List<ImportListMovie>>(), listId), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -149,9 +140,6 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Verify(v => v.SyncMoviesForList(It.IsAny<List<ImportListMovie>>(), listId), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -166,9 +154,6 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||
|
||||
var listResult = Subject.Fetch();
|
||||
listResult.AnyFailure.Should().BeTrue();
|
||||
|
||||
Mocker.GetMock<IImportListMovieService>()
|
||||
.Verify(v => v.SyncMoviesForList(It.IsAny<List<ImportListMovie>>(), passedListId), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.ImportExclusions;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
@@ -59,8 +60,7 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
_importListFetch = new ImportListFetchResult
|
||||
{
|
||||
Movies = _list1Movies,
|
||||
AnyFailure = false,
|
||||
SyncedLists = 1
|
||||
AnyFailure = false
|
||||
};
|
||||
|
||||
_commandAll = new ImportListSyncCommand
|
||||
@@ -84,6 +84,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
.Setup(v => v.MovieExists(It.IsAny<Movie>()))
|
||||
.Returns(false);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.MovieExists(It.IsAny<Movie>()))
|
||||
.Returns(false);
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.AllMovieTmdbIds())
|
||||
.Returns(new List<int>());
|
||||
@@ -91,6 +95,10 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
Mocker.GetMock<IFetchAndParseImportList>()
|
||||
.Setup(v => v.Fetch())
|
||||
.Returns(_importListFetch);
|
||||
|
||||
Mocker.GetMock<ISearchForNewMovie>()
|
||||
.Setup(v => v.MapMovieToTmdbMovie(It.IsAny<MovieMetadata>()))
|
||||
.Returns<MovieMetadata>(m => new MovieMetadata { TmdbId = m.TmdbId });
|
||||
}
|
||||
|
||||
private void GivenListFailure()
|
||||
@@ -100,7 +108,8 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
|
||||
private void GivenNoListSync()
|
||||
{
|
||||
_importListFetch.SyncedLists = 0;
|
||||
_importListFetch.SyncedLists = new List<int>();
|
||||
_importListFetch.SyncedWithoutFailure = new List<int>();
|
||||
}
|
||||
|
||||
private void GivenCleanLevel(string cleanLevel)
|
||||
@@ -114,6 +123,9 @@ namespace NzbDrone.Core.Test.ImportList
|
||||
{
|
||||
var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto };
|
||||
|
||||
_importListFetch.SyncedLists.Add(id);
|
||||
_importListFetch.SyncedWithoutFailure.Add(id);
|
||||
|
||||
Mocker.GetMock<IImportListFactory>()
|
||||
.Setup(v => v.Get(id))
|
||||
.Returns(importListDefinition);
|
||||
|
||||
@@ -364,6 +364,7 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||
[TestCase("German.Only.Movie.2021.French.1080p.BluRay.AVC-UNTAVC")]
|
||||
[TestCase("Movie.Title.2008.US.Directors.Cut.UHD.BD66.Blu-ray")]
|
||||
[TestCase("Movie.2009.Blu.ray.AVC.DTS.HD.MA.5.1")]
|
||||
[TestCase("[BD]Movie.Title.2008.2023.1080p.COMPLETE.BLURAY-RlsGrp")]
|
||||
public void should_parse_brdisk_1080p_quality(string title)
|
||||
{
|
||||
ParseAndVerifyQuality(title, QualitySource.BLURAY, false, Resolution.R1080p, Modifier.BRDISK);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.143" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.118-22" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Analytics
|
||||
{
|
||||
get
|
||||
{
|
||||
var lastRecord = _historyService.Paged(new PagingSpec<MovieHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
|
||||
var lastRecord = _historyService.Paged(new PagingSpec<MovieHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null, null);
|
||||
var monthAgo = DateTime.UtcNow.AddMonths(-1);
|
||||
|
||||
return lastRecord.Records.Any(v => v.Date > monthAgo);
|
||||
|
||||
@@ -66,7 +66,8 @@ namespace NzbDrone.Core.Annotations
|
||||
OAuth,
|
||||
Device,
|
||||
TagSelect,
|
||||
RootFolder
|
||||
RootFolder,
|
||||
QualityProfile
|
||||
}
|
||||
|
||||
public enum HiddenType
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
{
|
||||
public class QualityProfileSpecificationValidator : AbstractValidator<QualityProfileSpecification>
|
||||
{
|
||||
public QualityProfileSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class QualityProfileSpecification : AutoTaggingSpecificationBase
|
||||
{
|
||||
private static readonly QualityProfileSpecificationValidator Validator = new ();
|
||||
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Quality Profile";
|
||||
|
||||
[FieldDefinition(1, Label = "Quality Profile", Type = FieldType.QualityProfile)]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||
{
|
||||
return Value == movie.QualityProfileId;
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,13 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("AutoRedownloadFailed", value); }
|
||||
}
|
||||
|
||||
public bool AutoRedownloadFailedFromInteractiveSearch
|
||||
{
|
||||
get { return GetValueBoolean("AutoRedownloadFailedFromInteractiveSearch", true); }
|
||||
|
||||
set { SetValue("AutoRedownloadFailedFromInteractiveSearch", value); }
|
||||
}
|
||||
|
||||
public bool CreateEmptyMovieFolders
|
||||
{
|
||||
get { return GetValueBoolean("CreateEmptyMovieFolders", false); }
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace NzbDrone.Core.Configuration
|
||||
bool EnableCompletedDownloadHandling { get; set; }
|
||||
|
||||
bool AutoRedownloadFailed { get; set; }
|
||||
bool AutoRedownloadFailedFromInteractiveSearch { get; set; }
|
||||
|
||||
// Media Management
|
||||
bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; }
|
||||
|
||||
@@ -407,7 +407,7 @@ namespace NzbDrone.Core.Datastore
|
||||
return pagingSpec;
|
||||
}
|
||||
|
||||
private void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
|
||||
protected void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
|
||||
{
|
||||
var filters = pagingSpec.FilterExpressions;
|
||||
|
||||
|
||||
@@ -41,14 +41,16 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
private static string GetConnectionString(string dbPath)
|
||||
{
|
||||
var connectionBuilder = new SQLiteConnectionStringBuilder();
|
||||
|
||||
connectionBuilder.DataSource = dbPath;
|
||||
connectionBuilder.CacheSize = (int)-20000;
|
||||
connectionBuilder.DateTimeKind = DateTimeKind.Utc;
|
||||
connectionBuilder.JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal;
|
||||
connectionBuilder.Pooling = true;
|
||||
connectionBuilder.Version = 3;
|
||||
var connectionBuilder = new SQLiteConnectionStringBuilder
|
||||
{
|
||||
DataSource = dbPath,
|
||||
CacheSize = (int)-20000,
|
||||
DateTimeKind = DateTimeKind.Utc,
|
||||
JournalMode = OsInfo.IsOsx ? SQLiteJournalModeEnum.Truncate : SQLiteJournalModeEnum.Wal,
|
||||
Pooling = true,
|
||||
Version = 3,
|
||||
BusyTimeout = 100
|
||||
};
|
||||
|
||||
if (OsInfo.IsOsx)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
private static readonly Regex[] DiscRegex = new[]
|
||||
{
|
||||
new Regex(@"(?:dis[ck])(?:[-_. ]\d+[-_. ])(?:(?:(?:480|720|1080|2160)[ip]|)[-_. ])?(?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?:(?:480|720|1080|2160)[ip]|)[-_. ](?:full)[-_. ](?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
new Regex(@"(?:(?:480|720|1080|2160)[ip]|)[-_. ](?:full)[-_. ](?:Blu\-?ray)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?:\d?x?M?DVD-?[R59])", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
private static readonly string[] _dvdContainerTypes = new[] { "vob", "iso" };
|
||||
@@ -39,8 +40,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
if (regex.IsMatch(subject.Release.Title))
|
||||
{
|
||||
_logger.Debug("Release contains raw Bluray, rejecting.");
|
||||
return Decision.Reject("Raw Bluray release");
|
||||
_logger.Debug("Release contains raw Bluray/DVD, rejecting.");
|
||||
return Decision.Reject("Raw Bluray/DVD release");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
@@ -23,5 +24,6 @@ namespace NzbDrone.Core.Download
|
||||
public TrackedDownload TrackedDownload { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public bool SkipRedownload { get; set; }
|
||||
public ReleaseSourceType ReleaseSource { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
@@ -128,6 +130,7 @@ namespace NzbDrone.Core.Download
|
||||
private void PublishDownloadFailedEvent(List<MovieHistory> historyItems, string message, TrackedDownload trackedDownload = null, bool skipRedownload = false)
|
||||
{
|
||||
var historyItem = historyItems.First();
|
||||
Enum.TryParse(historyItem.Data.GetValueOrDefault(MovieHistory.RELEASE_SOURCE, ReleaseSourceType.Unknown.ToString()), out ReleaseSourceType releaseSource);
|
||||
|
||||
var downloadFailedEvent = new DownloadFailedEvent
|
||||
{
|
||||
@@ -140,7 +143,8 @@ namespace NzbDrone.Core.Download
|
||||
Data = historyItem.Data,
|
||||
TrackedDownload = trackedDownload,
|
||||
Languages = historyItem.Languages,
|
||||
SkipRedownload = skipRedownload
|
||||
SkipRedownload = skipRedownload,
|
||||
ReleaseSource = releaseSource
|
||||
};
|
||||
|
||||
_eventAggregator.PublishEvent(downloadFailedEvent);
|
||||
|
||||
@@ -5,6 +5,7 @@ using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Messaging;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
@@ -38,6 +39,12 @@ namespace NzbDrone.Core.Download
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.ReleaseSource == ReleaseSourceType.InteractiveSearch && !_configService.AutoRedownloadFailedFromInteractiveSearch)
|
||||
{
|
||||
_logger.Debug("Auto redownloading failed movies from interactive search is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.MovieId != 0)
|
||||
{
|
||||
_logger.Debug("Failed download contains a movie, searching again.");
|
||||
|
||||
@@ -7,7 +7,7 @@ using NzbDrone.Core.Movies.Events;
|
||||
namespace NzbDrone.Core.HealthCheck.Checks
|
||||
{
|
||||
[CheckOn(typeof(MovieUpdatedEvent))]
|
||||
[CheckOn(typeof(MoviesDeletedEvent), CheckOnCondition.FailedOnly)]
|
||||
[CheckOn(typeof(MoviesDeletedEvent))]
|
||||
[CheckOn(typeof(MovieRefreshCompleteEvent))]
|
||||
public class RemovedMovieCheck : HealthCheckBase, ICheckOnCondition<MovieUpdatedEvent>, ICheckOnCondition<MoviesDeletedEvent>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Messaging;
|
||||
@@ -28,6 +29,7 @@ namespace NzbDrone.Core.HealthCheck
|
||||
private readonly IProvideHealthCheck[] _scheduledHealthChecks;
|
||||
private readonly Dictionary<Type, IEventDrivenHealthCheck[]> _eventDrivenHealthChecks;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<HealthCheck> _healthCheckResults;
|
||||
private readonly HashSet<IProvideHealthCheck> _pendingHealthChecks;
|
||||
@@ -40,10 +42,12 @@ namespace NzbDrone.Core.HealthCheck
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheManager cacheManager,
|
||||
IDebounceManager debounceManager,
|
||||
IRuntimeInfo runtimeInfo)
|
||||
IRuntimeInfo runtimeInfo,
|
||||
Logger logger)
|
||||
{
|
||||
_healthChecks = healthChecks.ToArray();
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
|
||||
_healthCheckResults = cacheManager.GetCache<HealthCheck>(GetType());
|
||||
_pendingHealthChecks = new HashSet<IProvideHealthCheck>();
|
||||
@@ -88,7 +92,14 @@ namespace NzbDrone.Core.HealthCheck
|
||||
|
||||
try
|
||||
{
|
||||
var results = healthChecks.Select(c => c.Check())
|
||||
var results = healthChecks.Select(c =>
|
||||
{
|
||||
_logger.Trace("Check health -> {0}", c.GetType().Name);
|
||||
var result = c.Check();
|
||||
_logger.Trace("Check health <- {0}", c.GetType().Name);
|
||||
|
||||
return result;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var result in results)
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.History
|
||||
void DeleteForMovies(List<int> movieIds);
|
||||
MovieHistory MostRecentForMovie(int movieId);
|
||||
List<MovieHistory> Since(DateTime date, MovieHistoryEventType? eventType);
|
||||
PagingSpec<MovieHistory> GetPaged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities);
|
||||
}
|
||||
|
||||
public class HistoryRepository : BasicRepository<MovieHistory>, IHistoryRepository
|
||||
@@ -74,19 +75,6 @@ namespace NzbDrone.Core.History
|
||||
Delete(c => movieIds.Contains(c.MovieId));
|
||||
}
|
||||
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
|
||||
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
||||
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
|
||||
|
||||
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder sql) =>
|
||||
_database.QueryJoined<MovieHistory, Movie, QualityProfile>(sql, (hist, movie, profile) =>
|
||||
{
|
||||
hist.Movie = movie;
|
||||
hist.Movie.QualityProfile = profile;
|
||||
return hist;
|
||||
});
|
||||
|
||||
public MovieHistory MostRecentForMovie(int movieId)
|
||||
{
|
||||
return Query(x => x.MovieId == movieId).MaxBy(h => h.Date);
|
||||
@@ -106,5 +94,77 @@ namespace NzbDrone.Core.History
|
||||
|
||||
return PagedQuery(builder).OrderBy(h => h.Date).ToList();
|
||||
}
|
||||
|
||||
public PagingSpec<MovieHistory> GetPaged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
|
||||
{
|
||||
pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, languages, qualities), pagingSpec, PagedQuery);
|
||||
|
||||
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
|
||||
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, languages, qualities).Select(typeof(MovieHistory)), pagingSpec, countTemplate);
|
||||
|
||||
return pagingSpec;
|
||||
}
|
||||
|
||||
private SqlBuilder PagedBuilder(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
|
||||
{
|
||||
var builder = Builder()
|
||||
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
|
||||
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
||||
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
|
||||
|
||||
AddFilters(builder, pagingSpec);
|
||||
|
||||
if (languages is { Length: > 0 })
|
||||
{
|
||||
builder.Where($"({BuildLanguageWhereClause(languages)})");
|
||||
}
|
||||
|
||||
if (qualities is { Length: > 0 })
|
||||
{
|
||||
builder.Where($"({BuildQualityWhereClause(qualities)})");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder builder) =>
|
||||
_database.QueryJoined<MovieHistory, Movie, QualityProfile>(builder, (hist, movie, profile) =>
|
||||
{
|
||||
hist.Movie = movie;
|
||||
hist.Movie.QualityProfile = profile;
|
||||
return hist;
|
||||
});
|
||||
|
||||
private string BuildLanguageWhereClause(int[] languages)
|
||||
{
|
||||
var clauses = new List<string>();
|
||||
|
||||
foreach (var language in languages)
|
||||
{
|
||||
// There are 4 different types of values we should see:
|
||||
// - Not the last value in the array
|
||||
// - When it's the last value in the array and on different OSes
|
||||
// - When it was converted from a single language
|
||||
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language},%]'");
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(13) || '%]'");
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(10) || '%]'");
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[{language}]'");
|
||||
}
|
||||
|
||||
return $"({string.Join(" OR ", clauses)})";
|
||||
}
|
||||
|
||||
private string BuildQualityWhereClause(int[] qualities)
|
||||
{
|
||||
var clauses = new List<string>();
|
||||
|
||||
foreach (var quality in qualities)
|
||||
{
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'");
|
||||
}
|
||||
|
||||
return $"({string.Join(" OR ", clauses)})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace NzbDrone.Core.History
|
||||
public interface IHistoryService
|
||||
{
|
||||
QualityModel GetBestQualityInHistory(QualityProfile profile, int movieId);
|
||||
PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec);
|
||||
PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities);
|
||||
MovieHistory MostRecentForMovie(int movieId);
|
||||
MovieHistory MostRecentForDownloadId(string downloadId);
|
||||
MovieHistory Get(int historyId);
|
||||
@@ -49,9 +49,9 @@ namespace NzbDrone.Core.History
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec)
|
||||
public PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
|
||||
{
|
||||
return _historyRepository.GetPaged(pagingSpec);
|
||||
return _historyRepository.GetPaged(pagingSpec, languages, qualities);
|
||||
}
|
||||
|
||||
public MovieHistory MostRecentForMovie(int movieId)
|
||||
|
||||
@@ -5,9 +5,6 @@ using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists
|
||||
{
|
||||
@@ -21,26 +18,14 @@ namespace NzbDrone.Core.ImportLists
|
||||
{
|
||||
private readonly IImportListFactory _importListFactory;
|
||||
private readonly IImportListStatusService _importListStatusService;
|
||||
private readonly IImportListMovieService _listMovieService;
|
||||
private readonly ISearchForNewMovie _movieSearch;
|
||||
private readonly IProvideMovieInfo _movieInfoService;
|
||||
private readonly IMovieMetadataService _movieMetadataService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public FetchAndParseImportListService(IImportListFactory importListFactory,
|
||||
IImportListStatusService importListStatusService,
|
||||
IImportListMovieService listMovieService,
|
||||
ISearchForNewMovie movieSearch,
|
||||
IProvideMovieInfo movieInfoService,
|
||||
IMovieMetadataService movieMetadataService,
|
||||
Logger logger)
|
||||
{
|
||||
_importListFactory = importListFactory;
|
||||
_importListStatusService = importListStatusService;
|
||||
_listMovieService = listMovieService;
|
||||
_movieSearch = movieSearch;
|
||||
_movieInfoService = movieInfoService;
|
||||
_movieMetadataService = movieMetadataService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -101,21 +86,17 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
if (!importListReports.AnyFailure)
|
||||
{
|
||||
var alreadyMapped = result.Movies.Where(x => importListReports.Movies.Any(r => r.TmdbId == x.TmdbId));
|
||||
var listMovies = MapMovieReports(importListReports.Movies.Where(x => result.Movies.All(r => r.TmdbId != x.TmdbId))).Where(x => x.TmdbId > 0).ToList();
|
||||
var listMovies = importListReports.Movies;
|
||||
|
||||
listMovies.AddRange(alreadyMapped);
|
||||
listMovies = listMovies.DistinctBy(x => x.TmdbId).ToList();
|
||||
listMovies.ForEach(m => m.ListId = importList.Definition.Id);
|
||||
|
||||
result.Movies.AddRange(listMovies);
|
||||
_listMovieService.SyncMoviesForList(listMovies, importList.Definition.Id);
|
||||
|
||||
result.SyncedWithoutFailure.Add(importList.Definition.Id);
|
||||
}
|
||||
|
||||
result.AnyFailure |= importListReports.AnyFailure;
|
||||
result.SyncedLists++;
|
||||
|
||||
_importListStatusService.UpdateListSyncStatus(importList.Definition.Id);
|
||||
result.SyncedLists.Add(importList.Definition.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -129,9 +110,17 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
Task.WaitAll(taskList.ToArray());
|
||||
|
||||
foreach (var list in importLists)
|
||||
{
|
||||
if (result.SyncedLists.Contains(list.Definition.Id))
|
||||
{
|
||||
_importListStatusService.UpdateListSyncStatus(list.Definition.Id);
|
||||
}
|
||||
}
|
||||
|
||||
result.Movies = result.Movies.DistinctBy(r => new { r.TmdbId, r.ImdbId, r.Title }).ToList();
|
||||
|
||||
_logger.Debug("Found {0} total reports from {1} lists", result.Movies.Count, result.SyncedLists);
|
||||
_logger.Debug("Found {0} total reports from {1} lists", result.Movies.Count, result.SyncedLists.Count);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -160,19 +149,19 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
if (!importListReports.AnyFailure)
|
||||
{
|
||||
var listMovies = MapMovieReports(importListReports.Movies)
|
||||
.Where(x => x.TmdbId > 0)
|
||||
.DistinctBy(x => x.TmdbId)
|
||||
.ToList();
|
||||
var listMovies = importListReports.Movies;
|
||||
|
||||
listMovies.ForEach(m => m.ListId = importList.Definition.Id);
|
||||
|
||||
result.Movies.AddRange(listMovies);
|
||||
_listMovieService.SyncMoviesForList(listMovies, importList.Definition.Id);
|
||||
|
||||
result.SyncedWithoutFailure.Add(importList.Definition.Id);
|
||||
}
|
||||
|
||||
result.AnyFailure |= importListReports.AnyFailure;
|
||||
|
||||
result.SyncedLists.Add(importList.Definition.Id);
|
||||
|
||||
_importListStatusService.UpdateListSyncStatus(importList.Definition.Id);
|
||||
}
|
||||
}
|
||||
@@ -187,32 +176,5 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ImportListMovie> MapMovieReports(IEnumerable<ImportListMovie> reports)
|
||||
{
|
||||
var mappedMovies = reports.Select(m => _movieSearch.MapMovieToTmdbMovie(new MovieMetadata { Title = m.Title, TmdbId = m.TmdbId, ImdbId = m.ImdbId, Year = m.Year }))
|
||||
.Where(x => x != null)
|
||||
.DistinctBy(x => x.TmdbId)
|
||||
.ToList();
|
||||
|
||||
_movieMetadataService.UpsertMany(mappedMovies);
|
||||
|
||||
var mappedListMovies = new List<ImportListMovie>();
|
||||
|
||||
foreach (var movieMeta in mappedMovies)
|
||||
{
|
||||
var mappedListMovie = new ImportListMovie();
|
||||
|
||||
if (movieMeta != null)
|
||||
{
|
||||
mappedListMovie.MovieMetadata = movieMeta;
|
||||
mappedListMovie.MovieMetadataId = movieMeta.Id;
|
||||
}
|
||||
|
||||
mappedListMovies.Add(mappedListMovie);
|
||||
}
|
||||
|
||||
return mappedListMovies;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
@@ -8,6 +10,7 @@ namespace NzbDrone.Core.ImportLists.ImportExclusions
|
||||
{
|
||||
bool IsMovieExcluded(int tmdbid);
|
||||
ImportExclusion GetByTmdbid(int tmdbid);
|
||||
List<int> AllExcludedTmdbIds();
|
||||
}
|
||||
|
||||
public class ImportExclusionsRepository : BasicRepository<ImportExclusion>, IImportExclusionsRepository
|
||||
@@ -26,5 +29,12 @@ namespace NzbDrone.Core.ImportLists.ImportExclusions
|
||||
{
|
||||
return Query(x => x.TmdbId == tmdbid).First();
|
||||
}
|
||||
|
||||
public List<int> AllExcludedTmdbIds()
|
||||
{
|
||||
using var conn = _database.OpenConnection();
|
||||
|
||||
return conn.Query<int>("SELECT \"TmdbId\" FROM \"ImportExclusions\"").ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace NzbDrone.Core.ImportLists.ImportExclusions
|
||||
|
||||
public List<ImportExclusion> AddExclusions(List<ImportExclusion> exclusions)
|
||||
{
|
||||
_exclusionRepository.InsertMany(exclusions);
|
||||
_exclusionRepository.InsertMany(DeDupeExclusions(exclusions));
|
||||
|
||||
return exclusions;
|
||||
}
|
||||
@@ -76,8 +76,20 @@ namespace NzbDrone.Core.ImportLists.ImportExclusions
|
||||
if (message.AddExclusion)
|
||||
{
|
||||
_logger.Debug("Adding {0} Deleted Movies to Import Exclusions", message.Movies.Count);
|
||||
_exclusionRepository.InsertMany(message.Movies.Select(m => new ImportExclusion { TmdbId = m.TmdbId, MovieTitle = m.Title, MovieYear = m.Year }).ToList());
|
||||
|
||||
var exclusions = message.Movies.Select(m => new ImportExclusion { TmdbId = m.TmdbId, MovieTitle = m.Title, MovieYear = m.Year }).ToList();
|
||||
_exclusionRepository.InsertMany(DeDupeExclusions(exclusions));
|
||||
}
|
||||
}
|
||||
|
||||
private List<ImportExclusion> DeDupeExclusions(List<ImportExclusion> exclusions)
|
||||
{
|
||||
var existingExclusions = _exclusionRepository.AllExcludedTmdbIds();
|
||||
|
||||
return exclusions
|
||||
.DistinctBy(x => x.TmdbId)
|
||||
.Where(x => !existingExclusions.Contains(x.TmdbId))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ namespace NzbDrone.Core.ImportLists
|
||||
public ImportListFetchResult()
|
||||
{
|
||||
Movies = new List<ImportListMovie>();
|
||||
SyncedLists = new List<int>();
|
||||
SyncedWithoutFailure = new List<int>();
|
||||
}
|
||||
|
||||
public List<ImportListMovie> Movies { get; set; }
|
||||
public bool AnyFailure { get; set; }
|
||||
public int SyncedLists { get; set; }
|
||||
public List<int> SyncedLists { get; set; }
|
||||
public List<int> SyncedWithoutFailure { get; set; }
|
||||
}
|
||||
|
||||
public abstract class ImportListBase<TSettings> : IImportList
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.ImportLists.ImportExclusions;
|
||||
using NzbDrone.Core.ImportLists.ImportListMovies;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists
|
||||
@@ -17,6 +18,8 @@ namespace NzbDrone.Core.ImportLists
|
||||
private readonly IImportListFactory _importListFactory;
|
||||
private readonly IFetchAndParseImportList _listFetcherAndParser;
|
||||
private readonly IMovieService _movieService;
|
||||
private readonly IMovieMetadataService _movieMetadataService;
|
||||
private readonly ISearchForNewMovie _movieSearch;
|
||||
private readonly IAddMovieService _addMovieService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IImportExclusionsService _exclusionService;
|
||||
@@ -25,6 +28,8 @@ namespace NzbDrone.Core.ImportLists
|
||||
public ImportListSyncService(IImportListFactory importListFactory,
|
||||
IFetchAndParseImportList listFetcherAndParser,
|
||||
IMovieService movieService,
|
||||
IMovieMetadataService movieMetadataService,
|
||||
ISearchForNewMovie movieSearch,
|
||||
IAddMovieService addMovieService,
|
||||
IConfigService configService,
|
||||
IImportExclusionsService exclusionService,
|
||||
@@ -34,6 +39,8 @@ namespace NzbDrone.Core.ImportLists
|
||||
_importListFactory = importListFactory;
|
||||
_listFetcherAndParser = listFetcherAndParser;
|
||||
_movieService = movieService;
|
||||
_movieMetadataService = movieMetadataService;
|
||||
_movieSearch = movieSearch;
|
||||
_addMovieService = addMovieService;
|
||||
_exclusionService = exclusionService;
|
||||
_listMovieService = listMovieService;
|
||||
@@ -43,26 +50,26 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
private void SyncAll()
|
||||
{
|
||||
if (_importListFactory.Enabled().Where(a => ((ImportListDefinition)a.Definition).EnableAuto).Empty())
|
||||
if (_importListFactory.Enabled().Empty())
|
||||
{
|
||||
_logger.Debug("No import lists with automatic add enabled, skipping sync and cleaning");
|
||||
_logger.Debug("No enabled import lists, skipping sync and cleaning");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var listItemsResult = _listFetcherAndParser.Fetch();
|
||||
|
||||
if (listItemsResult.SyncedLists == 0)
|
||||
if (listItemsResult.SyncedLists.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessListItems(listItemsResult);
|
||||
|
||||
if (!listItemsResult.AnyFailure)
|
||||
{
|
||||
CleanLibrary();
|
||||
}
|
||||
|
||||
ProcessListItems(listItemsResult);
|
||||
}
|
||||
|
||||
private void SyncList(ImportListDefinition definition)
|
||||
@@ -125,7 +132,25 @@ namespace NzbDrone.Core.ImportLists
|
||||
|
||||
private void ProcessListItems(ImportListFetchResult listFetchResult)
|
||||
{
|
||||
listFetchResult.Movies = listFetchResult.Movies.DistinctBy(x =>
|
||||
var allMappedMovies = new List<ImportListMovie>();
|
||||
|
||||
// Sync ListMovies table for Discovery view and Cleaning task
|
||||
foreach (var listId in listFetchResult.SyncedWithoutFailure)
|
||||
{
|
||||
var listMovies = listFetchResult.Movies.Where(x => x.ListId == listId);
|
||||
var alreadyMapped = allMappedMovies.Where(x => listMovies.Any(r => r.TmdbId == x.TmdbId));
|
||||
var mappedListMovies = MapMovieReports(listMovies.Where(x => allMappedMovies.All(r => r.TmdbId != x.TmdbId)).ToList()).Where(x => x.TmdbId > 0).ToList();
|
||||
|
||||
mappedListMovies.AddRange(alreadyMapped);
|
||||
mappedListMovies = mappedListMovies.DistinctBy(x => x.TmdbId).ToList();
|
||||
mappedListMovies.ForEach(m => m.ListId = listId);
|
||||
|
||||
allMappedMovies.AddRange(mappedListMovies);
|
||||
|
||||
_listMovieService.SyncMoviesForList(mappedListMovies, listId);
|
||||
}
|
||||
|
||||
allMappedMovies = allMappedMovies.DistinctBy(x =>
|
||||
{
|
||||
if (x.TmdbId != 0)
|
||||
{
|
||||
@@ -140,7 +165,7 @@ namespace NzbDrone.Core.ImportLists
|
||||
return x.Title;
|
||||
}).ToList();
|
||||
|
||||
var listedMovies = listFetchResult.Movies.ToList();
|
||||
var listedMovies = allMappedMovies;
|
||||
|
||||
var importExclusions = _exclusionService.GetAllExclusions();
|
||||
var dbMovies = _movieService.AllMovieTmdbIds();
|
||||
@@ -168,6 +193,33 @@ namespace NzbDrone.Core.ImportLists
|
||||
}
|
||||
}
|
||||
|
||||
private List<ImportListMovie> MapMovieReports(IEnumerable<ImportListMovie> reports)
|
||||
{
|
||||
var mappedMovies = reports.Select(m => _movieSearch.MapMovieToTmdbMovie(new MovieMetadata { Title = m.Title, TmdbId = m.TmdbId, ImdbId = m.ImdbId, Year = m.Year }))
|
||||
.Where(x => x != null)
|
||||
.DistinctBy(x => x.TmdbId)
|
||||
.ToList();
|
||||
|
||||
_movieMetadataService.UpsertMany(mappedMovies);
|
||||
|
||||
var mappedListMovies = new List<ImportListMovie>();
|
||||
|
||||
foreach (var movieMeta in mappedMovies)
|
||||
{
|
||||
var mappedListMovie = new ImportListMovie();
|
||||
|
||||
if (movieMeta != null)
|
||||
{
|
||||
mappedListMovie.MovieMetadata = movieMeta;
|
||||
mappedListMovie.MovieMetadataId = movieMeta.Id;
|
||||
}
|
||||
|
||||
mappedListMovies.Add(mappedListMovie);
|
||||
}
|
||||
|
||||
return mappedListMovies;
|
||||
}
|
||||
|
||||
public void Execute(ImportListSyncCommand message)
|
||||
{
|
||||
if (message.DefinitionId.HasValue)
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
"RefreshInformationAndScanDisk": "تحديث المعلومات ومسح القرص",
|
||||
"RefreshAndScan": "التحديث والمسح الضوئي",
|
||||
"Refresh": "تحديث",
|
||||
"Redownload": "إعادة التنزيل",
|
||||
"RecyclingBinCleanup": "تنظيف سلة إعادة التدوير",
|
||||
"RecyclingBin": "صندوق إعادة التدوير",
|
||||
"RecycleBinHelpText": "ستنتقل ملفات الأفلام إلى هنا عند حذفها بدلاً من حذفها نهائيًا",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"ReadTheWikiForMoreInformation": "Прочетете Wiki за повече информация",
|
||||
"Reason": "Причина",
|
||||
"RecyclingBinCleanup": "Почистване на кошчето за рециклиране",
|
||||
"Redownload": "Презареждане",
|
||||
"Refresh": "Обнови",
|
||||
"RefreshMovie": "Опресняване на филма",
|
||||
"RegularExpressionsCanBeTested": "Регулярните изрази могат да бъдат тествани ",
|
||||
|
||||
@@ -730,7 +730,6 @@
|
||||
"RecycleBinCleanupDaysHelpTextWarning": "Els fitxers de la paperera de reciclatge més antics que el nombre de dies seleccionat es netejaran automàticament",
|
||||
"RecycleBinHelpText": "Els fitxers de pel·lícula aniran aquí quan se suprimeixin en lloc de suprimir-se permanentment",
|
||||
"RecyclingBinCleanup": "Neteja de la paperera de reciclatge",
|
||||
"Redownload": "Torna a baixar",
|
||||
"RefreshCollections": "Actualitza col·leccions",
|
||||
"RefreshInformationAndScanDisk": "Actualitza la informació i escaneja el disc",
|
||||
"RefreshLists": "Actualitza llistes",
|
||||
|
||||
@@ -315,7 +315,6 @@
|
||||
"ReadTheWikiForMoreInformation": "Další informace najdete na Wiki",
|
||||
"Reason": "Důvod",
|
||||
"RecyclingBinCleanup": "Recyklace koše Vyčištění",
|
||||
"Redownload": "Znovu stáhnout",
|
||||
"Refresh": "Obnovit",
|
||||
"RefreshMovie": "Obnovit film",
|
||||
"RegularExpressionsCanBeTested": "Regulární výrazy lze testovat ",
|
||||
|
||||
@@ -313,7 +313,6 @@
|
||||
"ReadTheWikiForMoreInformation": "Læs Wiki for mere information",
|
||||
"Reason": "Grund",
|
||||
"RecyclingBinCleanup": "Oprydning af papirkurven",
|
||||
"Redownload": "Genindlæs",
|
||||
"Refresh": "Opdater",
|
||||
"RefreshMovie": "Opdater film",
|
||||
"RegularExpressionsCanBeTested": "Regulære udtryk kan testes ",
|
||||
|
||||
@@ -461,7 +461,6 @@
|
||||
"RecycleBinHelpText": "Gelöschte Filmdateien werden hierher verschoben anstatt sie direkt endgültig zu löschen",
|
||||
"RecyclingBin": "Papierkorb",
|
||||
"RecyclingBinCleanup": "Papierkorb aufräumen",
|
||||
"Redownload": "Nochmal herunterladen",
|
||||
"RefreshInformationAndScanDisk": "Metadaten aktualisieren und Festplatte scannen",
|
||||
"RefreshMovie": "Film aktualisieren",
|
||||
"ReleaseRejected": "Release abgelehnt",
|
||||
@@ -960,7 +959,7 @@
|
||||
"AddRootFolder": "Stammordner hinzufügen",
|
||||
"AddQualityProfile": "Qualitätsprofil hinzufügen",
|
||||
"AddedToDownloadQueue": "Zur Downloadwarteschlange hinzugefügt",
|
||||
"AddDownloadClient": "Zum Downloader hinzufügen",
|
||||
"AddDownloadClient": "Downloadmanager hinzufügen",
|
||||
"AddCustomFormat": "Eigenes Format hinzufügen",
|
||||
"AddDelayProfile": "Verzögerungsprofil hinzufügen",
|
||||
"Add": "Hinzufügen",
|
||||
@@ -1170,12 +1169,14 @@
|
||||
"ApplyTagsHelpTextHowToApplyImportLists": "Wie werden Tags zu ausgewählten Filmen zugeteilt",
|
||||
"DeleteRootFolderMessageText": "Indexer '{0}' wirklich löschen?",
|
||||
"DeleteRootFolder": "Stammordner löschen",
|
||||
"AddConnection": "Sammlung bearbeiten",
|
||||
"AddConnection": "Verbindung hinzufügen",
|
||||
"BypassDelayIfAboveCustomFormatScoreMinimumScore": "Minimum der eigenen Formate Bewertungspunkte",
|
||||
"NotificationStatusAllClientHealthCheckMessage": "Wegen Fehlern sind keine Applikationen verfügbar",
|
||||
"NotificationStatusSingleClientHealthCheckMessage": "Applikationen wegen folgender Fehler nicht verfügbar: {0}",
|
||||
"RemoveFromDownloadClientHelpTextWarning": "Dies wird den Download und alle bereits heruntergeladenen Dateien aus dem Downloader entfernen.",
|
||||
"DownloadClientsLoadError": "Downloader konnten nicht geladen werden",
|
||||
"IMDbId": "TMDb ID",
|
||||
"DisabledForLocalAddresses": "Für Lokale Adressen deaktivieren"
|
||||
"DisabledForLocalAddresses": "Für Lokale Adressen deaktivieren",
|
||||
"AddCondition": "Bedingung hinzufügen",
|
||||
"AddAutoTag": "Automatisches Tag hinzufügen"
|
||||
}
|
||||
|
||||
@@ -197,7 +197,6 @@
|
||||
"AutomaticSearch": "Αυτόματη αναζήτηση",
|
||||
"ChmodGroup": "Ομάδα chmod",
|
||||
"Queued": "Σε ουρά",
|
||||
"Redownload": "Κατεβάστε ξανά",
|
||||
"Refresh": "Φρεσκάρω",
|
||||
"RelativePath": "Σχετική διαδρομή",
|
||||
"Released": "Κυκλοφόρησε",
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Enter a new username",
|
||||
"AuthenticationRequiredWarning": "To prevent remote access without authentication, {appName} now requires authentication to be enabled. You can optionally disable authentication from local addresses.",
|
||||
"Auto": "Auto",
|
||||
"AutoRedownloadFailed": "Redownload Failed",
|
||||
"AutoRedownloadFailedFromInteractiveSearch": "Redownload Failed from Interactive Search",
|
||||
"AutoRedownloadFailedFromInteractiveSearchHelpText": "Automatically search for and attempt to download a different release when failed release was grabbed from interactive search",
|
||||
"AutoRedownloadFailedHelpText": "Automatically search for and attempt to download a different release",
|
||||
"AutoTagging": "Auto Tagging",
|
||||
"AutoTaggingNegateHelpText": "If checked, the auto tagging rule will not apply if this {0} condition matches.",
|
||||
@@ -878,6 +881,7 @@
|
||||
"QualitySettings": "Quality Settings",
|
||||
"QualitySettingsSummary": "Quality sizes and naming",
|
||||
"Queue": "Queue",
|
||||
"QueueFilterHasNoItems": "Selected queue filter has no items",
|
||||
"QueueIsEmpty": "Queue is empty",
|
||||
"QueueLoadError": "Failed to load Queue",
|
||||
"Queued": "Queued",
|
||||
@@ -908,7 +912,6 @@
|
||||
"RecyclingBin": "Recycling Bin",
|
||||
"RecyclingBinCleanup": "Recycling Bin Cleanup",
|
||||
"Reddit": "Reddit",
|
||||
"Redownload": "Redownload",
|
||||
"Refresh": "Refresh",
|
||||
"RefreshAndScan": "Refresh & Scan",
|
||||
"RefreshCollections": "Refresh Collections",
|
||||
|
||||
@@ -479,7 +479,6 @@
|
||||
"ReleaseDates": "Fechas de Estreno",
|
||||
"RefreshMovie": "Actualizar película",
|
||||
"RefreshInformationAndScanDisk": "Actualizar la información al escanear el disco",
|
||||
"Redownload": "Volver a descargar",
|
||||
"RecyclingBinCleanup": "Limpieza de Papelera de Reciclaje",
|
||||
"RecyclingBin": "Papelera de Reciclaje",
|
||||
"RecycleBinHelpText": "Los archivos iran aquí una vez se hayan borrado en vez de ser borrados permanentemente",
|
||||
|
||||
@@ -324,7 +324,6 @@
|
||||
"AlternativeTitle": "Vaihtoehtoinen nimi",
|
||||
"Age": "Ikä",
|
||||
"RecyclingBinCleanup": "Roskakorin tyhjennys",
|
||||
"Redownload": "Lataa uudelleen",
|
||||
"RefreshMovie": "Päivitä elokuva",
|
||||
"RejectionCount": "Hylkäämisluku",
|
||||
"RelativePath": "Suhteellinen polku",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -227,7 +227,6 @@
|
||||
"ReadTheWikiForMoreInformation": "קרא את הוויקי למידע נוסף",
|
||||
"Reason": "סיבה",
|
||||
"RecyclingBinCleanup": "ניקוי סל המיחזור",
|
||||
"Redownload": "הורד מחדש",
|
||||
"Refresh": "לְרַעֲנֵן",
|
||||
"RefreshMovie": "רענן סרט",
|
||||
"RejectionCount": "ספירת דחייה",
|
||||
|
||||
@@ -442,7 +442,6 @@
|
||||
"Presets": "प्रीसेट",
|
||||
"Profiles": "प्रोफाइल",
|
||||
"Reason": "कारण",
|
||||
"Redownload": "redownload",
|
||||
"Refresh": "ताज़ा करना",
|
||||
"RefreshMovie": "फिल्म को रिफ्रेश करें",
|
||||
"Reset": "रीसेट",
|
||||
|
||||
@@ -137,9 +137,9 @@
|
||||
"CertificateValidation": "Tanúsítvány érvényesítése",
|
||||
"Cast": "Szereplők",
|
||||
"CantFindMovie": "Miért nem találom a filmemet?",
|
||||
"CancelProcessing": "Feldolgozás Megállítása",
|
||||
"CancelProcessing": "Folyamat leállítása",
|
||||
"CancelPendingTask": "Biztosan törlöd ezt a függőben lévő feladatot?",
|
||||
"Cancel": "Vissza",
|
||||
"Cancel": "Mégse",
|
||||
"Calendar": "Naptár",
|
||||
"BypassProxyForLocalAddresses": "Proxy megkerülése a helyi hálózatos címekhez",
|
||||
"BranchUpdateMechanism": "A külső frissítési mechanizmus által használt ágazat",
|
||||
@@ -148,7 +148,7 @@
|
||||
"BindAddressHelpText": "Érvényes IP-cím, localhost vagy '*' minden interfészhez",
|
||||
"BindAddress": "Kapcsolási Cím",
|
||||
"BeforeUpdate": "Alkalmazásfrissítés előtt",
|
||||
"Backups": "Biztonsági Mentés",
|
||||
"Backups": "Biztonsági mentések",
|
||||
"BackupRetentionHelpText": "A megőrzési időnél régebbi automatikus biztonsági másolatok automatikusan törlésre kerülnek",
|
||||
"BackupNow": "Biztonsági Mentés Most",
|
||||
"BackupIntervalHelpText": "Időeltérés a biztonsági mentések között",
|
||||
@@ -158,7 +158,7 @@
|
||||
"AvailabilityDelay": "Elérhetőség Késleltetése",
|
||||
"AutoUnmonitorPreviouslyDownloadedMoviesHelpText": "A lemezről törölt filmeket a Radarr automatikusan eltávolítja a megfigyelt filmek közül",
|
||||
"AutoRedownloadFailedHelpText": "Másik kiadás automatikus keresése és letöltése",
|
||||
"AutomaticSearch": "Automatikus Keresés",
|
||||
"AutomaticSearch": "Automatikus keresés",
|
||||
"Automatic": "Automatikus",
|
||||
"AnalyticsEnabledHelpText": "Küldjön névtelen használati és hibainformációkat a Radarr szervereire. Ez magában foglalja a böngészőjéről szóló információkat, mely Radarr WebUI oldalakat használja, a hibajelentést, valamint az operációs rendszer adatait. Ezeket az információkat a funkciók és a hibajavítások rangsorolására használjuk fel.",
|
||||
"AuthenticationMethodHelpText": "Felhasználónév és Jelszó szükséges a Radarr-hoz való hozzáféréshez",
|
||||
@@ -167,7 +167,7 @@
|
||||
"AsAllDayHelpText": "Az események egész napos eseményként jelennek meg a naptáradban",
|
||||
"ApplyTags": "Címkék alkalmazása",
|
||||
"AgeWhenGrabbed": "Kora (mikor hozzáadásra került)",
|
||||
"Age": "Kora",
|
||||
"Age": "Kor",
|
||||
"AddListExclusion": "Kizárási Lista Hozzáadása",
|
||||
"ApiKey": "API Kulcs",
|
||||
"All": "Összes",
|
||||
@@ -392,7 +392,6 @@
|
||||
"RefreshInformationAndScanDisk": "Információk frissítése és lemez átvizsgálása",
|
||||
"RefreshAndScan": "Frissítés & Keresés",
|
||||
"Refresh": "Frissítés",
|
||||
"Redownload": "Letöltés újra",
|
||||
"RecyclingBinCleanup": "Lomtár kiürítése",
|
||||
"RecyclingBin": "Lomtár",
|
||||
"RecycleBinHelpText": "A filmfájlok végleges törlés helyett ide kerülnek törléskor",
|
||||
@@ -742,7 +741,7 @@
|
||||
"Unmonitored": "Nem felügyelt",
|
||||
"UnmappedFolders": "Feltérképezetlen mappák",
|
||||
"Ungroup": "Csoport eltávolítása",
|
||||
"Unavailable": "Nem érhető el",
|
||||
"Unavailable": "Nem elérhető",
|
||||
"UnableToLoadUISettings": "Nem sikerült betölteni a felhasználói felület beállításait",
|
||||
"UnableToLoadTheCalendar": "Nem sikerült betölteni a naptárat",
|
||||
"UnableToLoadTags": "Nem sikerült betölteni a címkéket",
|
||||
@@ -818,7 +817,7 @@
|
||||
"CurrentlyInstalled": "Jelenleg telepítve",
|
||||
"Connection": "Kapcsolat",
|
||||
"CertValidationNoLocal": "Letiltva a helyi címeknél",
|
||||
"CalendarOptions": "Naptár opciók",
|
||||
"CalendarOptions": "Naptár beállítások",
|
||||
"BuiltIn": "Beépített",
|
||||
"AuthForm": "Felhasználó (Bejelentkezési oldal)",
|
||||
"AuthBasic": "Alap (Böngésző felugró ablak)",
|
||||
@@ -830,7 +829,7 @@
|
||||
"AfterManualRefresh": "A kézi frissítés után",
|
||||
"AddToDownloadQueue": "Hozzáadás a letöltési sorhoz",
|
||||
"AddRootFolder": "Gyökérmappa hozzáadása",
|
||||
"AddQualityProfile": "Minőségi profil hozzáadása",
|
||||
"AddQualityProfile": "Minőségi Profil hozzáadása",
|
||||
"AddedToDownloadQueue": "Hozzáadva a letöltési sorhoz",
|
||||
"AddDownloadClient": "Letöltőkliens hozzáadása",
|
||||
"AddDelayProfile": "Késleltetési profil hozzáadása",
|
||||
@@ -1081,7 +1080,7 @@
|
||||
"RottenTomatoesRating": "Tomato Értékelés",
|
||||
"TotalMovies": "Összes film",
|
||||
"ApplicationUrlHelpText": "Az alkalmazás külső URL-címe, beleértve a http(s)://-t, a portot és az URL-alapot",
|
||||
"ApplicationURL": "Alkalmazás URL-je",
|
||||
"ApplicationURL": "Alkalmazás URL",
|
||||
"PreferredProtocol": "Preferált protokoll",
|
||||
"SettingsTheme": "Téma",
|
||||
"SettingsThemeHelpText": "Változtasd meg az alkalmazás felhasználói felület témáját, az „Auto” téma az operációs rendszer témáját használja a Világos vagy Sötét mód beállításához. A Theme.Park ihlette",
|
||||
@@ -1102,5 +1101,10 @@
|
||||
"DeleteCustomFormatMessageText": "Biztosan törölni akarod a/az '{0}' egyéni formátumot?",
|
||||
"RemoveSelectedItemsQueueMessageText": "Biztosan el akar távolítani {0} elemet a várólistáról?",
|
||||
"RemoveSelectedItemQueueMessageText": "Biztosan el akar távolítani 1 elemet a várólistáról?",
|
||||
"ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {0} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban"
|
||||
"ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {0} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban",
|
||||
"AuthenticationRequiredPasswordHelpTextWarning": "Adjon meg új jelszót",
|
||||
"AuthenticationRequiredUsernameHelpTextWarning": "Adjon meg új felhasználónevet",
|
||||
"AutomaticAdd": "Automatikus hozzáadás",
|
||||
"Unknown": "Ismeretlen",
|
||||
"UnknownEventTooltip": "Ismeretlen Esemény"
|
||||
}
|
||||
|
||||
@@ -289,7 +289,6 @@
|
||||
"RadarrCalendarFeed": "Radarr dagatalstraumur",
|
||||
"ReadTheWikiForMoreInformation": "Lestu Wiki fyrir frekari upplýsingar",
|
||||
"RecyclingBinCleanup": "Hreinsun ruslakörfu",
|
||||
"Redownload": "Endurhlaða",
|
||||
"Refresh": "Hressa",
|
||||
"RefreshMovie": "Hressa kvikmynd",
|
||||
"RelativePath": "Hlutfallsleg leið",
|
||||
|
||||
@@ -415,7 +415,6 @@
|
||||
"RegularExpressionsCanBeTested": "Le espressioni regolari possono essere testate ",
|
||||
"RefreshMovie": "Aggiorna il Film",
|
||||
"RefreshInformationAndScanDisk": "Aggiorna le informazioni e scansiona il disco",
|
||||
"Redownload": "Riscarica",
|
||||
"RecyclingBinCleanup": "Pulizia del cestino",
|
||||
"RecyclingBin": "Cestino",
|
||||
"RecycleBinHelpText": "I file dei film andranno qui quando cancellati invece che venire eliminati definitivamente",
|
||||
|
||||
@@ -268,7 +268,6 @@
|
||||
"ReadTheWikiForMoreInformation": "詳細については、Wikiをお読みください",
|
||||
"Reason": "理由",
|
||||
"RecyclingBinCleanup": "ごみ箱のクリーンアップ",
|
||||
"Redownload": "再ダウンロード",
|
||||
"Refresh": "更新",
|
||||
"RefreshMovie": "映画を更新する",
|
||||
"RegularExpressionsCanBeTested": "正規表現をテストできます ",
|
||||
|
||||
@@ -270,7 +270,6 @@
|
||||
"ReadTheWikiForMoreInformation": "자세한 내용은 Wiki를 참조하십시오.",
|
||||
"Reason": "이유",
|
||||
"RecyclingBinCleanup": "재활용 빈 정리",
|
||||
"Redownload": "다시 다운로드",
|
||||
"Refresh": "새롭게 하다",
|
||||
"RefreshMovie": "영화 새로 고침",
|
||||
"RegularExpressionsCanBeTested": "정규식을 테스트 할 수 있습니다. ",
|
||||
|
||||
@@ -512,7 +512,6 @@
|
||||
"RecycleBinCleanupDaysHelpTextWarning": "Bestanden in de prullenbak ouder dan het geselecteerde aantal dagen zullen automatisch opgeschoond worden",
|
||||
"RecyclingBin": "Prullenbak",
|
||||
"RecyclingBinCleanup": "Prullenbak Opruimen",
|
||||
"Redownload": "Opnieuw downloaden",
|
||||
"RefreshInformationAndScanDisk": "Informatie vernieuwen en schijf herscannen",
|
||||
"RefreshMovie": "Film vernieuwen",
|
||||
"ProxyType": "Proxy Type",
|
||||
|
||||
@@ -272,7 +272,6 @@
|
||||
"RadarrCalendarFeed": "Kanał kalendarza radarowego",
|
||||
"Reason": "Powód",
|
||||
"RecyclingBinCleanup": "Czyszczenie kosza na śmieci",
|
||||
"Redownload": "Pobierz ponownie",
|
||||
"Refresh": "Odświeżać",
|
||||
"RefreshMovie": "Odśwież film",
|
||||
"RegularExpressionsCanBeTested": "Można testować wyrażenia regularne ",
|
||||
|
||||
@@ -533,7 +533,6 @@
|
||||
"Remove": "Remover",
|
||||
"RefreshMovie": "Atualizar filme",
|
||||
"RefreshInformationAndScanDisk": "Atualizar informações e analisar o disco",
|
||||
"Redownload": "Transferir novamente",
|
||||
"RecyclingBinCleanup": "Limpeza da reciclagem",
|
||||
"RecyclingBin": "Reciclagem",
|
||||
"Reason": "Razão",
|
||||
@@ -802,7 +801,7 @@
|
||||
"AddCustomFormat": "Adicionar formato personalizado",
|
||||
"AddDelayProfile": "Adicionar perfil de atraso",
|
||||
"AddDownloadClient": "Adicionar cliente de transferências",
|
||||
"AllMoviesInPathHaveBeenImported": "Todos os filmes em {0} foram importados",
|
||||
"AllMoviesInPathHaveBeenImported": "Todos os filmes no {caminho} foram importados",
|
||||
"AllResultsHiddenFilter": "Todos os resultados estão ocultos pelo filtro aplicado",
|
||||
"Always": "Sempre",
|
||||
"AptUpdater": "Utilize o apt para instalar a atualização",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user