1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-05 13:21:25 -05:00

Compare commits

...

22 Commits

Author SHA1 Message Date
Bogdan
e0b93a03fd Remove create_test_cases.py 2025-05-17 01:17:00 +03:00
Mark McDowall
f7f5837d49 Convert Missing to TypeScript
(cherry picked from commit 3035521b93ef54a6cc6193a526be862976228669)
2025-05-16 19:38:18 +03:00
Mark McDowall
c3ee8b3c90 Convert Cutoff Unmet to TypeScript
(cherry picked from commit 45c53bea865447aa543242e64e3d796c93117975)
2025-05-16 19:31:49 +03:00
Weblate
4de78e3bab Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-05-15 22:42:37 +03:00
Bogdan
426538c8af Remove console statement 2025-05-15 14:56:25 +03:00
Bogdan
c82404c75b Fixed: Loading suggestions for header search input 2025-05-15 14:48:40 +03:00
Bogdan
9bee9841c1 Fixed: (PTP) Download torrent files with API credentials 2025-05-15 00:58:38 +03:00
Bogdan
010959d915 Bump @babel/runtime 2025-05-14 18:48:34 +03:00
Bogdan
a600728916 Bump react-virtualized to 9.22.6
Bump @types/react
2025-05-14 18:37:10 +03:00
Bogdan
bbfb8c7cc2 Bump babel, fontawesome icons, fuse.js, react-lazyload, react-use-measure and react-window 2025-05-14 14:37:12 +03:00
Bogdan
32418ea521 Bump core-js to 3.42 2025-05-14 14:37:12 +03:00
Bogdan
2c5c99e9b7 New: Deprecate use of movie file tokens in Movie Folder Format 2025-05-13 14:41:32 +03:00
Bogdan
a5e5a63e45 Fixed: Upgrade notification title for Apprise 2025-05-13 00:43:46 +03:00
Bogdan
31b44d2c2e New: Include movie poster for Apprise 2025-05-12 22:54:42 +03:00
Bogdan
da8e8a12de New: Include year in interactive searches title
Fixes #11070
2025-05-12 22:50:32 +03:00
v3DJG6GL
6506c97ce1 Fixed: Map SwissGerman to German (#11068)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-05-12 16:50:38 +03:00
v3DJG6GL
5303a1992c New: Add Romansh language 2025-05-11 13:36:26 +03:00
Bogdan
042308c319 Bump version to 5.23.3 2025-05-11 10:53:03 +03:00
Bogdan
2e97e09f44 Fail build on missing test results
Ignore missing test results failure on FreeBSD
2025-05-10 13:46:35 +03:00
Bogdan
ccfb9c0dad Bump SixLabors.ImageSharp to 3.1.8 2025-05-10 13:43:52 +03:00
Weblate
b655d97e9e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Robi Korb <robi.korb@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2025-05-04 21:05:34 +03:00
Bogdan
3afcb91db6 Bump version to 5.23.2 2025-05-04 21:04:29 +03:00
61 changed files with 2266 additions and 2281 deletions

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.23.1'
majorVersion: '5.23.3'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -481,6 +481,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: ne(variables['testName'], 'freebsd-x64')
- job: Unit_Docker
displayName: Unit Docker
@@ -540,7 +541,8 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
- job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
@@ -596,6 +598,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
@@ -652,6 +655,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
- stage: Integration
displayName: Integration
@@ -734,6 +738,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres14
@@ -796,6 +801,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
@@ -859,6 +865,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
- job: Integration_FreeBSD
@@ -905,6 +912,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'FreeBSD Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: false
displayName: Publish Test Results
- job: Integration_Docker
@@ -974,6 +982,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
- stage: Automation
@@ -1055,6 +1064,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(osName) Automation Tests'
failTaskOnFailedTests: $(failBuild)
failTaskOnMissingResultsFile: $(failBuild)
displayName: Publish Test Results
- stage: Analyze

View File

@@ -1,44 +0,0 @@
input1 = """Prometheus.Special.Edition.Fan Edit.2012..BRRip.x264.AAC-m2g
Star Wars Episode IV - A New Hope (Despecialized) 1999.mkv
Prometheus.(Special.Edition.Remastered).2012.[Bluray-1080p].mkv
Prometheus Extended 2012
Prometheus Extended Directors Cut Fan Edit 2012
Prometheus Director's Cut 2012
Prometheus Directors Cut 2012
Prometheus.(Extended.Theatrical.Version.IMAX).BluRay.1080p.2012.asdf
2001 A Space Odyssey Director's Cut (1968).mkv
2001: A Space Odyssey (Extended Directors Cut FanEdit) Bluray 1080p 1968
A Fake Movie 2035 Directors 2012.mkv
Blade Runner Director's Cut 2049.mkv
Prometheus 50th Anniversary Edition 2012.mkv
Movie 2in1 2012.mkv
Movie IMAX 2012.mkv"""
output1 = """Special.Edition.Fan Edit BRRip.x264.AAC-m2g
Despecialized mkv
Special.Edition.Remastered Bluray-1080p].mkv
Extended mkv
Extended Directors Cut Fan Edit mkv
Director's Cut mkv
Directors Cut mkv
Extended.Theatrical.Version.IMAX asdf
Director's Cut mkv
Extended Directors Cut FanEdit mkv
Directors mkv
Director's Cut mkv
50th Anniversary Edition mkv
2in1 mkv
IMAX mkv"""
inputs = input1.split("\n")
outputs = output1.split("\n")
real_o = []
for output in outputs:
real_o.append(output.split(" ")[0].replace(".", " ").strip())
count = 0
for inp in inputs:
o = real_o[count]
print "[TestCase(\"{0}\", \"{1}\")]".format(inp, o)
count += 1

View File

@@ -176,7 +176,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: '3.41'
corejs: '3.42'
}
]
]

View File

@@ -32,8 +32,8 @@ import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet';
import Missing from 'Wanted/Missing/Missing';
function RedirectWithUrlBase() {
return <Redirect to={getPathWithUrlBase('/')} />;
@@ -89,9 +89,9 @@ function AppRoutes() {
Wanted
*/}
<Route path="/wanted/missing" component={MissingConnector} />
<Route path="/wanted/missing" component={Missing} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmet} />
{/*
Settings

View File

@@ -24,6 +24,7 @@ import RootFolderAppState from './RootFolderAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
import WantedAppState from './WantedAppState';
interface FilterBuilderPropOption {
id: string;
@@ -47,14 +48,14 @@ export interface PropertyFilter {
export interface Filter {
key: string;
label: string | (() => string);
filers: PropertyFilter[];
filters: PropertyFilter[];
}
export interface CustomFilter {
id: number;
type: string;
label: string;
filers: PropertyFilter[];
filters: PropertyFilter[];
}
export interface AppSectionState {
@@ -105,6 +106,7 @@ interface AppState {
settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState;
wanted: WantedAppState;
}
export default AppState;

View File

@@ -0,0 +1,29 @@
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from 'App/State/AppSectionState';
import Movie from 'Movie/Movie';
interface WantedMovie extends Movie {
isSaving?: boolean;
}
interface WantedCutoffUnmetAppState
extends AppSectionState<WantedMovie>,
AppSectionFilterState<WantedMovie>,
PagedAppSectionState,
TableAppSectionState {}
interface WantedMissingAppState
extends AppSectionState<WantedMovie>,
AppSectionFilterState<WantedMovie>,
PagedAppSectionState,
TableAppSectionState {}
interface WantedAppState {
cutoffUnmet: WantedCutoffUnmetAppState;
missing: WantedMissingAppState;
}
export default WantedAppState;

View File

@@ -14,7 +14,7 @@ import {
RenderSuggestion,
SuggestionsFetchRequestedParams,
} from 'react-autosuggest';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import { useDebouncedCallback } from 'use-debounce';
import { Kind } from 'Helpers/Props/kinds';
import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from '../AutoSuggestInput';

View File

@@ -13,10 +13,10 @@ import React, {
import Autosuggest from 'react-autosuggest';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useDebouncedCallback } from 'use-debounce';
import { Tag } from 'App/State/TagsAppState';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
import { icons } from 'Helpers/Props';
import Movie from 'Movie/Movie';

View File

@@ -129,7 +129,6 @@ function VirtualTable<T extends ModelBase>({
>
{header}
{/* @ts-expect-error - ref type is incompatible */}
<div ref={registerChild}>
<Grid
{...otherProps}

View File

@@ -1,16 +0,0 @@
import { debounce, DebouncedFunc, DebounceSettings } from 'lodash';
import { useCallback } from 'react';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useDebouncedCallback<T extends (...args: any) => any>(
callback: T,
delay: number,
options?: DebounceSettings
): DebouncedFunc<T> {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(debounce(callback, delay, options), [
callback,
delay,
options,
]);
}

View File

@@ -968,7 +968,6 @@ function MovieDetails({ movieId }: MovieDetailsProps) {
<MovieInteractiveSearchModal
isOpen={isInteractiveSearchModalOpen}
movieId={id}
movieTitle={title}
onModalClose={handleInteractiveSearchModalClose}
/>
</PageContentBody>

View File

@@ -6,6 +6,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import { MovieEntity } from 'Movie/useMovie';
import { executeCommand } from 'Store/Actions/commandActions';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import translate from 'Utilities/String/translate';
@@ -14,12 +15,10 @@ import styles from './MovieSearchCell.css';
interface MovieSearchCellProps {
movieId: number;
movieTitle: string;
movieEntity?: MovieEntity;
}
function MovieSearchCell(props: MovieSearchCellProps) {
const { movieId, movieTitle } = props;
function MovieSearchCell({ movieId }: MovieSearchCellProps) {
const executingCommands = useSelector(createExecutingCommandsSelector());
const isSearching = executingCommands.some(({ name, body }) => {
const { movieIds = [] } = body;
@@ -61,7 +60,6 @@ function MovieSearchCell(props: MovieSearchCellProps) {
<MovieInteractiveSearchModal
isOpen={isInteractiveSearchModalOpen}
movieId={movieId}
movieTitle={movieTitle}
onModalClose={setInteractiveSearchModalClosed}
/>
</TableRowCell>

View File

@@ -17,9 +17,11 @@ interface MovieInteractiveSearchModalProps
isOpen: boolean;
}
function MovieInteractiveSearchModal(props: MovieInteractiveSearchModalProps) {
const { isOpen, movieId, movieTitle, onModalClose } = props;
function MovieInteractiveSearchModal({
isOpen,
movieId,
onModalClose,
}: MovieInteractiveSearchModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
@@ -41,7 +43,6 @@ function MovieInteractiveSearchModal(props: MovieInteractiveSearchModalProps) {
>
<MovieInteractiveSearchModalContent
movieId={movieId}
movieTitle={movieTitle}
onModalClose={handleModalClose}
/>
</Modal>

View File

@@ -7,6 +7,8 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props';
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
import Movie from 'Movie/Movie';
import useMovie from 'Movie/useMovie';
import { clearMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieHistory } from 'Store/Actions/movieHistoryActions';
import {
@@ -17,17 +19,17 @@ import translate from 'Utilities/String/translate';
export interface MovieInteractiveSearchModalContentProps {
movieId: number;
movieTitle?: string;
onModalClose(): void;
}
function MovieInteractiveSearchModalContent({
movieId,
movieTitle,
onModalClose,
}: MovieInteractiveSearchModalContentProps) {
const dispatch = useDispatch();
const { title, year } = useMovie(movieId) as Movie;
useEffect(() => {
return () => {
dispatch(cancelFetchReleases());
@@ -38,6 +40,8 @@ function MovieInteractiveSearchModalContent({
};
}, [dispatch]);
const movieTitle = `${title}${year > 0 ? ` (${year})` : ''}`;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>

View File

@@ -249,6 +249,9 @@ function Naming() {
translate('MovieFolderFormatHelpText'),
...movieFolderFormatHelpTexts,
]}
helpTextWarning={translate(
'MovieFolderFormatHelpTextDeprecatedWarning'
)}
errors={[
...movieFolderFormatErrors,
...settings.movieFolderFormat.errors,

View File

@@ -1,4 +1,11 @@
export default function getFilterValue(filters, filterKey, filterValueKey, defaultValue) {
import { Filter } from 'App/State/AppState';
export default function getFilterValue<T>(
filters: Filter[],
filterKey: string | number,
filterValueKey: string,
defaultValue: T
) {
const filter = filters.find((f) => f.key === filterKey);
if (!filter) {

View File

@@ -1,292 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
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 CutoffUnmetRow from './CutoffUnmetRow';
function getMonitoredValue(props) {
const {
filters,
selectedFilterKey
} = props;
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
}
class CutoffUnmet extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmSearchAllCutoffUnmetModalOpen: false,
isInteractiveImportModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
});
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onFilterMenuItemPress = (filterKey, filterValue) => {
this.props.onFilterSelect(filterKey, filterValue);
};
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onSearchSelectedPress = () => {
const selected = this.getSelectedIds();
this.props.onSearchSelectedPress(selected);
};
onToggleSelectedPress = () => {
const movieIds = this.getSelectedIds();
this.props.batchToggleCutoffUnmetMovies({
movieIds,
monitored: !getMonitoredValue(this.props)
});
};
onSearchAllCutoffUnmetPress = () => {
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
};
onSearchAllCutoffUnmetConfirmed = () => {
const {
selectedFilterKey,
onSearchAllCutoffUnmetPress
} = this.props;
// TODO: Custom filters will need to check whether there is a monitored
// filter once implemented.
onSearchAllCutoffUnmetPress(selectedFilterKey === 'monitored');
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
};
onConfirmSearchAllCutoffUnmetModalClose = () => {
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
selectedFilterKey,
filters,
columns,
totalRecords,
isSearchingForCutoffUnmetMovies,
isSaving,
onFilterSelect,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmSearchAllCutoffUnmetModalOpen
} = this.state;
const itemsSelected = !!this.getSelectedIds().length;
const isShowingMonitored = getMonitoredValue(this.props);
return (
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
iconName={icons.SEARCH}
isDisabled={isSearchingForCutoffUnmetMovies}
isSpinning={isSearchingForCutoffUnmetMovies}
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={this.onToggleSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('CutoffUnmetLoadError')}
</Alert>
}
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('CutoffUnmetNoItems')}
</Alert>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<CutoffUnmetRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
<ConfirmModal
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmetMovies')}
message={
<div>
<div>
{translate('SearchForCutoffUnmetMoviesConfirmationCount', { totalRecords })}
</div>
<div>
{translate('MassSearchCancelWarning')}
</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
CutoffUnmet.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isSearchingForCutoffUnmetMovies: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSearchSelectedPress: PropTypes.func.isRequired,
batchToggleCutoffUnmetMovies: PropTypes.func.isRequired,
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
};
export default CutoffUnmet;

View File

@@ -0,0 +1,358 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState, { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import {
batchToggleCutoffUnmetMovies,
clearCutoffUnmet,
fetchCutoffUnmet,
gotoCutoffUnmetPage,
setCutoffUnmetFilter,
setCutoffUnmetSort,
setCutoffUnmetTableOption,
} from 'Store/Actions/wantedActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import CutoffUnmetRow from './CutoffUnmetRow';
function getMonitoredValue(
filters: Filter[],
selectedFilterKey: string
): boolean {
return !!getFilterValue(filters, selectedFilterKey, 'monitored', false);
}
function CutoffUnmet() {
const dispatch = useDispatch();
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords = 0,
} = useSelector((state: AppState) => state.wanted.cutoffUnmet);
const isSearchingForAllMovies = useSelector(
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH)
);
const isSearchingForSelectedMovies = useSelector(
createCommandExecutingSelector(commandNames.MOVIE_SEARCH)
);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] =
useState(false);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoCutoffUnmetPage,
});
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const isSaving = useMemo(() => {
return items.filter((m) => m.isSaving).length > 1;
}, [items]);
const itemsSelected = !!selectedIds.length;
const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey);
const isSearchingForMovies =
isSearchingForAllMovies || isSearchingForSelectedMovies;
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleSearchSelectedPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.MOVIE_SEARCH,
movieIds: selectedIds,
commandFinished: () => {
dispatch(fetchCutoffUnmet());
},
})
);
}, [selectedIds, dispatch]);
const handleSearchAllPress = useCallback(() => {
setIsConfirmSearchAllModalOpen(true);
}, []);
const handleConfirmSearchAllCutoffUnmetModalClose = useCallback(() => {
setIsConfirmSearchAllModalOpen(false);
}, []);
const handleSearchAllCutoffUnmetConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
commandFinished: () => {
dispatch(fetchCutoffUnmet());
},
})
);
setIsConfirmSearchAllModalOpen(false);
}, [dispatch]);
const handleToggleSelectedPress = useCallback(() => {
dispatch(
batchToggleCutoffUnmetMovies({
movieIds: selectedIds,
monitored: !isShowingMonitored,
})
);
}, [isShowingMonitored, selectedIds, dispatch]);
const handleFilterSelect = useCallback(
(filterKey: number | string) => {
dispatch(setCutoffUnmetFilter({ selectedFilterKey: filterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setCutoffUnmetSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setCutoffUnmetTableOption(payload));
if (payload.pageSize) {
dispatch(gotoCutoffUnmetPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchCutoffUnmet());
} else {
dispatch(gotoCutoffUnmetPage({ page: 1 }));
}
return () => {
dispatch(clearCutoffUnmet());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchCutoffUnmet());
};
registerPagePopulator(repopulate, [
'movieUpdated',
'movieFileUpdated',
'movieFileDeleted',
]);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
return (
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={
itemsSelected
? translate('SearchSelected')
: translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForMovies}
isSpinning={isSearchingForMovies}
onPress={
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('CutoffUnmetLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('CutoffUnmetNoItems')}</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<CutoffUnmetRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmetMovies')}
message={
<div>
<div>
{translate('SearchForCutoffUnmetMoviesConfirmationCount', {
totalRecords,
})}
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllCutoffUnmetConfirmed}
onCancel={handleConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
);
}
export default CutoffUnmet;

View File

@@ -1,188 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import * as wantedActions from 'Store/Actions/wantedActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import CutoffUnmet from './CutoffUnmet';
function createMapStateToProps() {
return createSelector(
(state) => state.wanted.cutoffUnmet,
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
(cutoffUnmet, isSearchingForCutoffUnmetMovies, isSearchingForSelectedCutoffUnmetMovies) => {
return {
isSearchingForCutoffUnmetMovies: isSearchingForCutoffUnmetMovies || isSearchingForSelectedCutoffUnmetMovies,
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
...cutoffUnmet
};
}
);
}
const mapDispatchToProps = {
...wantedActions,
executeCommand,
fetchQueueDetails,
clearQueueDetails,
fetchMovieFiles,
clearMovieFiles
};
class CutoffUnmetConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchCutoffUnmet,
gotoCutoffUnmetFirstPage
} = this.props;
registerPagePopulator(this.repopulate, ['movieUpdated', 'movieFileUpdated', 'movieFileDeleted']);
if (useCurrentPage) {
fetchCutoffUnmet();
} else {
gotoCutoffUnmetFirstPage();
}
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const movieIds = selectUniqueIds(this.props.items, 'id');
const movieFileIds = selectUniqueIds(this.props.items, 'movieFileId');
this.props.fetchQueueDetails({ movieIds });
if (movieFileIds.length) {
this.props.fetchMovieFiles({ movieFileIds });
}
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearCutoffUnmet();
this.props.clearQueueDetails();
this.props.clearMovieFiles();
}
//
// Control
repopulate = () => {
this.props.fetchCutoffUnmet();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoCutoffUnmetFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoCutoffUnmetPreviousPage();
};
onNextPagePress = () => {
this.props.gotoCutoffUnmetNextPage();
};
onLastPagePress = () => {
this.props.gotoCutoffUnmetLastPage();
};
onPageSelect = (page) => {
this.props.gotoCutoffUnmetPage({ page });
};
onSortPress = (sortKey) => {
this.props.setCutoffUnmetSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setCutoffUnmetFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setCutoffUnmetTableOption(payload);
if (payload.pageSize) {
this.props.gotoCutoffUnmetFirstPage();
}
};
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.MOVIE_SEARCH,
movieIds: selected,
commandFinished: this.repopulate
});
};
onSearchAllCutoffUnmetPress = (monitored) => {
this.props.executeCommand({
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
monitored,
commandFinished: this.repopulate
});
};
//
// Render
render() {
return (
<CutoffUnmet
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onSearchSelectedPress={this.onSearchSelectedPress}
onToggleSelectedPress={this.onToggleSelectedPress}
onSearchAllCutoffUnmetPress={this.onSearchAllCutoffUnmetPress}
{...this.props}
/>
);
}
}
CutoffUnmetConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchCutoffUnmet: PropTypes.func.isRequired,
gotoCutoffUnmetFirstPage: PropTypes.func.isRequired,
gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired,
gotoCutoffUnmetNextPage: PropTypes.func.isRequired,
gotoCutoffUnmetLastPage: PropTypes.func.isRequired,
gotoCutoffUnmetPage: PropTypes.func.isRequired,
setCutoffUnmetSort: PropTypes.func.isRequired,
setCutoffUnmetFilter: PropTypes.func.isRequired,
setCutoffUnmetTableOption: PropTypes.func.isRequired,
clearCutoffUnmet: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired,
fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector)
);

View File

@@ -1,172 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
import styles from './CutoffUnmetRow.css';
function CutoffUnmetRow(props) {
const {
id,
movieFileId,
year,
title,
titleSlug,
inCinemas,
digitalRelease,
physicalRelease,
lastSearchTime,
isSelected,
columns,
onSelectedChange
} = props;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={titleSlug}
title={title}
/>
</TableRowCell>
);
}
if (name === 'movieMetadata.year') {
return (
<TableRowCell key={name}>
{year}
</TableRowCell>
);
}
if (name === 'movieMetadata.inCinemas') {
return (
<RelativeDateCell
key={name}
className={styles[name]}
date={inCinemas}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.digitalRelease') {
return (
<RelativeDateCell
key={name}
className={styles[name]}
date={digitalRelease}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.physicalRelease') {
return (
<RelativeDateCell
key={name}
className={styles[name]}
date={physicalRelease}
timeForToday={false}
/>
);
}
if (name === 'languages') {
return (
<TableRowCell
key={name}
className={styles.languages}
>
<MovieFileLanguages
movieFileId={movieFileId}
/>
</TableRowCell>
);
}
if (name === 'movies.lastSearchTime') {
return (
<RelativeDateCell
key={name}
date={lastSearchTime}
includeSeconds={true}
/>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<MovieSearchCell
key={name}
movieId={id}
movieTitle={title}
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
/>
);
}
return null;
})
}
</TableRow>
);
}
CutoffUnmetRow.propTypes = {
id: PropTypes.number.isRequired,
movieFileId: PropTypes.number,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
lastSearchTime: PropTypes.string,
titleSlug: PropTypes.string.isRequired,
inCinemas: PropTypes.string,
digitalRelease: PropTypes.string,
physicalRelease: PropTypes.string,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default CutoffUnmetRow;

View File

@@ -0,0 +1,150 @@
import React from 'react';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
import { SelectStateInputProps } from 'typings/props';
import styles from './CutoffUnmetRow.css';
interface CutoffUnmetRowProps {
id: number;
movieFileId?: number;
inCinemas?: string;
digitalRelease?: string;
physicalRelease?: string;
lastSearchTime?: string;
title: string;
year: number;
titleSlug: string;
isSelected?: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
}
function CutoffUnmetRow({
id,
movieFileId,
inCinemas,
digitalRelease,
physicalRelease,
lastSearchTime,
title,
year,
titleSlug,
isSelected,
columns,
onSelectedChange,
}: CutoffUnmetRowProps) {
if (!movieFileId) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink titleSlug={titleSlug} title={title} />
</TableRowCell>
);
}
if (name === 'movieMetadata.year') {
return <TableRowCell key={name}>{year}</TableRowCell>;
}
if (name === 'movieMetadata.inCinemas') {
return (
<RelativeDateCell
key={name}
date={inCinemas}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.digitalRelease') {
return (
<RelativeDateCell
key={name}
date={digitalRelease}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.physicalRelease') {
return (
<RelativeDateCell
key={name}
date={physicalRelease}
timeForToday={false}
/>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name} className={styles.languages}>
<MovieFileLanguages movieFileId={movieFileId} />
</TableRowCell>
);
}
if (name === 'movies.lastSearchTime') {
return (
<RelativeDateCell
key={name}
date={lastSearchTime}
includeSeconds={true}
/>
);
}
if (name === 'status') {
return (
<TableRowCell key={name} className={styles.status}>
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity="wanted.cutoffUnmet"
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<MovieSearchCell
key={name}
movieId={id}
movieEntity="wanted.cutoffUnmet"
/>
);
}
return null;
})}
</TableRow>
);
}
export default CutoffUnmetRow;

View File

@@ -1,312 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
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 MissingRow from './MissingRow';
function getMonitoredValue(props) {
const {
filters,
selectedFilterKey
} = props;
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
}
class Missing extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmSearchAllMissingModalOpen: false,
isInteractiveImportModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
});
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onSearchSelectedPress = () => {
const selected = this.getSelectedIds();
this.props.onSearchSelectedPress(selected);
};
onToggleSelectedPress = () => {
const movieIds = this.getSelectedIds();
this.props.batchToggleMissingMovies({
movieIds,
monitored: !getMonitoredValue(this.props)
});
};
onSearchAllMissingPress = () => {
this.setState({ isConfirmSearchAllMissingModalOpen: true });
};
onSearchAllMissingConfirmed = () => {
const {
selectedFilterKey,
onSearchAllMissingPress
} = this.props;
// TODO: Custom filters will need to check whether there is a monitored
// filter once implemented.
onSearchAllMissingPress(selectedFilterKey === 'monitored');
this.setState({ isConfirmSearchAllMissingModalOpen: false });
};
onConfirmSearchAllMissingModalClose = () => {
this.setState({ isConfirmSearchAllMissingModalOpen: false });
};
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
};
onInteractiveImportModalClose = () => {
this.setState({ isInteractiveImportModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
selectedFilterKey,
filters,
columns,
totalRecords,
isSearchingForMissingMovies,
isSaving,
onFilterSelect,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmSearchAllMissingModalOpen,
isInteractiveImportModalOpen
} = this.state;
const itemsSelected = !!this.getSelectedIds().length;
const isShowingMonitored = getMonitoredValue(this.props);
return (
<PageContent title={translate('Missing')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
iconName={icons.SEARCH}
isSpinning={isSearchingForMissingMovies}
isDisabled={isSearchingForMissingMovies}
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllMissingPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={this.onToggleSelectedPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('ManualImport')}
iconName={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('MissingLoadError')}
</Alert>
}
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('MissingNoItems')}
</Alert>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<MissingRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
<ConfirmModal
isOpen={isConfirmSearchAllMissingModalOpen}
kind={kinds.DANGER}
title={translate('SearchForAllMissingMovies')}
message={
<div>
<div>
{translate('SearchForAllMissingMoviesConfirmationCount', { totalRecords })}
</div>
<div>
{translate('MassSearchCancelWarning')}
</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={this.onSearchAllMissingConfirmed}
onCancel={this.onConfirmSearchAllMissingModalClose}
/>
</div>
}
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
onModalClose={this.onInteractiveImportModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
Missing.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isSearchingForMissingMovies: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSearchSelectedPress: PropTypes.func.isRequired,
batchToggleMissingMovies: PropTypes.func.isRequired,
onSearchAllMissingPress: PropTypes.func.isRequired
};
export default Missing;

View File

@@ -0,0 +1,383 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState, { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import { executeCommand } from 'Store/Actions/commandActions';
import {
batchToggleMissingMovies,
clearMissing,
fetchMissing,
gotoMissingPage,
setMissingFilter,
setMissingSort,
setMissingTableOption,
} from 'Store/Actions/wantedActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import MissingRow from './MissingRow';
function getMonitoredValue(
filters: Filter[],
selectedFilterKey: string
): boolean {
return !!getFilterValue(filters, selectedFilterKey, 'monitored', false);
}
function Missing() {
const dispatch = useDispatch();
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords = 0,
} = useSelector((state: AppState) => state.wanted.missing);
const isSearchingForAllMovies = useSelector(
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH)
);
const isSearchingForSelectedMovies = useSelector(
createCommandExecutingSelector(commandNames.MOVIE_SEARCH)
);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] =
useState(false);
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
useState(false);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoMissingPage,
});
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const isSaving = useMemo(() => {
return items.filter((m) => m.isSaving).length > 1;
}, [items]);
const itemsSelected = !!selectedIds.length;
const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey);
const isSearchingForMovies =
isSearchingForAllMovies || isSearchingForSelectedMovies;
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleSearchSelectedPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.MOVIE_SEARCH,
movieIds: selectedIds,
commandFinished: () => {
dispatch(fetchMissing());
},
})
);
}, [selectedIds, dispatch]);
const handleSearchAllPress = useCallback(() => {
setIsConfirmSearchAllModalOpen(true);
}, []);
const handleConfirmSearchAllMissingModalClose = useCallback(() => {
setIsConfirmSearchAllModalOpen(false);
}, []);
const handleSearchAllMissingConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.MISSING_MOVIES_SEARCH,
commandFinished: () => {
dispatch(fetchMissing());
},
})
);
setIsConfirmSearchAllModalOpen(false);
}, [dispatch]);
const handleToggleSelectedPress = useCallback(() => {
dispatch(
batchToggleMissingMovies({
movieIds: selectedIds,
monitored: !isShowingMonitored,
})
);
}, [isShowingMonitored, selectedIds, dispatch]);
const handleInteractiveImportPress = useCallback(() => {
setIsInteractiveImportModalOpen(true);
}, []);
const handleInteractiveImportModalClose = useCallback(() => {
setIsInteractiveImportModalOpen(false);
}, []);
const handleFilterSelect = useCallback(
(filterKey: number | string) => {
dispatch(setMissingFilter({ selectedFilterKey: filterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setMissingSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setMissingTableOption(payload));
if (payload.pageSize) {
dispatch(gotoMissingPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchMissing());
} else {
dispatch(gotoMissingPage({ page: 1 }));
}
return () => {
dispatch(clearMissing());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchMissing());
};
registerPagePopulator(repopulate, [
'movieUpdated',
'movieFileUpdated',
'movieFileDeleted',
]);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
return (
<PageContent title={translate('Missing')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={
itemsSelected
? translate('SearchSelected')
: translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForMovies}
isSpinning={isSearchingForMovies}
onPress={
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('ManualImport')}
iconName={icons.INTERACTIVE}
onPress={handleInteractiveImportPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('MissingLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('MissingNoItems')}</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<MissingRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForAllMissingMovies')}
message={
<div>
<div>
{translate('SearchForAllMissingMoviesConfirmationCount', {
totalRecords,
})}
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllMissingConfirmed}
onCancel={handleConfirmSearchAllMissingModalClose}
/>
</div>
) : null}
</PageContentBody>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
onModalClose={handleInteractiveImportModalClose}
/>
</PageContent>
);
}
export default Missing;

View File

@@ -1,176 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import * as wantedActions from 'Store/Actions/wantedActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Missing from './Missing';
function createMapStateToProps() {
return createSelector(
(state) => state.wanted.missing,
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
(missing, isSearchingForMissingMovies, isSearchingForSelectedMissingMovies) => {
return {
isSearchingForMissingMovies: isSearchingForMissingMovies || isSearchingForSelectedMissingMovies,
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
...missing
};
}
);
}
const mapDispatchToProps = {
...wantedActions,
executeCommand,
fetchQueueDetails,
clearQueueDetails
};
class MissingConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchMissing,
gotoMissingFirstPage
} = this.props;
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
if (useCurrentPage) {
fetchMissing();
} else {
gotoMissingFirstPage();
}
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const movieIds = selectUniqueIds(this.props.items, 'id');
this.props.fetchQueueDetails({ movieIds });
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearMissing();
this.props.clearQueueDetails();
}
//
// Control
repopulate = () => {
this.props.fetchMissing();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoMissingFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoMissingPreviousPage();
};
onNextPagePress = () => {
this.props.gotoMissingNextPage();
};
onLastPagePress = () => {
this.props.gotoMissingLastPage();
};
onPageSelect = (page) => {
this.props.gotoMissingPage({ page });
};
onSortPress = (sortKey) => {
this.props.setMissingSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setMissingFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setMissingTableOption(payload);
if (payload.pageSize) {
this.props.gotoMissingFirstPage();
}
};
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.MOVIE_SEARCH,
movieIds: selected,
commandFinished: this.repopulate
});
};
onSearchAllMissingPress = (monitored) => {
this.props.executeCommand({
name: commandNames.MISSING_MOVIES_SEARCH,
monitored,
commandFinished: this.repopulate
});
};
//
// Render
render() {
return (
<Missing
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onSearchSelectedPress={this.onSearchSelectedPress}
onSearchAllMissingPress={this.onSearchAllMissingPress}
{...this.props}
/>
);
}
}
MissingConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchMissing: PropTypes.func.isRequired,
gotoMissingFirstPage: PropTypes.func.isRequired,
gotoMissingPreviousPage: PropTypes.func.isRequired,
gotoMissingNextPage: PropTypes.func.isRequired,
gotoMissingLastPage: PropTypes.func.isRequired,
gotoMissingPage: PropTypes.func.isRequired,
setMissingSort: PropTypes.func.isRequired,
setMissingFilter: PropTypes.func.isRequired,
setMissingTableOption: PropTypes.func.isRequired,
clearMissing: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(MissingConnector)
);

View File

@@ -1,162 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import styles from './MissingRow.css';
function MissingRow(props) {
const {
id,
movieFileId,
year,
title,
titleSlug,
inCinemas,
digitalRelease,
physicalRelease,
lastSearchTime,
isSelected,
columns,
onSelectedChange
} = props;
if (!title) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={titleSlug}
title={title}
/>
</TableRowCell>
);
}
if (name === 'movieMetadata.year') {
return (
<TableRowCell key={name}>
{year}
</TableRowCell>
);
}
if (name === 'movieMetadata.inCinemas') {
return (
<RelativeDateCell
key={name}
className={styles[name]}
date={inCinemas}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.digitalRelease') {
return (
<RelativeDateCell
key={name}
className={styles[name]}
date={digitalRelease}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.physicalRelease') {
return (
<RelativeDateCell
key={name}
className={styles[name]}
date={physicalRelease}
timeForToday={false}
/>
);
}
if (name === 'movies.lastSearchTime') {
return (
<RelativeDateCell
key={name}
date={lastSearchTime}
includeSeconds={true}
/>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity={movieEntities.WANTED_MISSING}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<MovieSearchCell
key={name}
movieId={id}
movieTitle={title}
movieEntity={movieEntities.WANTED_MISSING}
/>
);
}
return null;
})
}
</TableRow>
);
}
MissingRow.propTypes = {
id: PropTypes.number.isRequired,
movieFileId: PropTypes.number,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
lastSearchTime: PropTypes.string,
titleSlug: PropTypes.string.isRequired,
inCinemas: PropTypes.string,
digitalRelease: PropTypes.string,
physicalRelease: PropTypes.string,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default MissingRow;

View File

@@ -0,0 +1,141 @@
import React from 'react';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import { SelectStateInputProps } from 'typings/props';
import styles from './MissingRow.css';
interface MissingRowProps {
id: number;
movieFileId?: number;
inCinemas?: string;
digitalRelease?: string;
physicalRelease?: string;
lastSearchTime?: string;
title: string;
year: number;
titleSlug: string;
isSelected?: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
}
function MissingRow({
id,
movieFileId,
inCinemas,
digitalRelease,
physicalRelease,
lastSearchTime,
title,
year,
titleSlug,
isSelected,
columns,
onSelectedChange,
}: MissingRowProps) {
if (!title) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink titleSlug={titleSlug} title={title} />
</TableRowCell>
);
}
if (name === 'movieMetadata.year') {
return <TableRowCell key={name}>{year}</TableRowCell>;
}
if (name === 'movieMetadata.inCinemas') {
return (
<RelativeDateCell
key={name}
date={inCinemas}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.digitalRelease') {
return (
<RelativeDateCell
key={name}
date={digitalRelease}
timeForToday={false}
/>
);
}
if (name === 'movieMetadata.physicalRelease') {
return (
<RelativeDateCell
key={name}
date={physicalRelease}
timeForToday={false}
/>
);
}
if (name === 'movies.lastSearchTime') {
return (
<RelativeDateCell
key={name}
date={lastSearchTime}
includeSeconds={true}
/>
);
}
if (name === 'status') {
return (
<TableRowCell key={name} className={styles.status}>
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity="wanted.missing"
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<MovieSearchCell
key={name}
movieId={id}
movieEntity="wanted.missing"
/>
);
}
return null;
})}
</TableRow>
);
}
export default MissingRow;

View File

@@ -22,11 +22,11 @@
"defaults"
],
"dependencies": {
"@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-brands-svg-icons": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/fontawesome-free": "6.7.2",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.25",
@@ -34,14 +34,14 @@
"@sentry/integrations": "7.119.1",
"@tanstack/react-query": "5.74.3",
"@types/node": "20.16.11",
"@types/react": "18.3.12",
"@types/react": "18.3.21",
"@types/react-dom": "18.3.1",
"classnames": "2.5.1",
"connected-react-router": "6.9.3",
"copy-to-clipboard": "3.3.3",
"element-class": "0.2.2",
"filesize": "10.1.6",
"fuse.js": "7.0.0",
"fuse.js": "7.1.0",
"history": "4.10.1",
"jdu": "1.0.0",
"jquery": "3.7.1",
@@ -65,7 +65,7 @@
"react-dom": "18.3.1",
"react-focus-lock": "2.9.4",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0",
"react-lazyload": "3.2.1",
"react-measure": "1.4.7",
"react-popper": "1.3.7",
"react-redux": "7.2.4",
@@ -74,9 +74,9 @@
"react-slider": "1.1.4",
"react-tabs": "4.3.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.1",
"react-virtualized": "9.21.1",
"react-window": "1.8.10",
"react-use-measure": "2.1.7",
"react-virtualized": "9.22.6",
"react-window": "1.8.11",
"redux": "4.2.1",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
@@ -85,16 +85,17 @@
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"swiper": "8.3.2",
"typescript": "5.7.2"
"typescript": "5.7.2",
"use-debounce": "10.0.4"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/core": "7.27.1",
"@babel/eslint-parser": "7.27.1",
"@babel/plugin-proposal-export-default-from": "7.27.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@types/lodash": "4.14.195",
"@types/mousetrap": "1.6.15",
"@types/react-autosuggest": "10.1.11",
@@ -103,7 +104,7 @@
"@types/react-lazyload": "3.2.3",
"@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.19.0",
"@types/react-virtualized": "9.22.0",
"@types/react-virtualized": "9.22.2",
"@types/react-window": "1.8.8",
"@types/redux-actions": "2.6.5",
"@types/webpack-livereload-plugin": "2.3.6",
@@ -113,7 +114,7 @@
"babel-loader": "9.2.1",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.41.0",
"core-js": "3.42.0",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.1",

View File

@@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
first.Guid.Should().Be("PassThePopcorn-452135");
first.Title.Should().Be("The.Night.Of.S01.BluRay.AAC2.0.x264-DEPTH");
first.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
first.DownloadUrl.Should().Be("https://passthepopcorn.me/torrents.php?action=download&id=452135&authkey=00000000000000000000000000000000&torrent_pass=00000000000000000000000000000000");
first.DownloadUrl.Should().Be("https://passthepopcorn.me/torrents.php?action=download&id=452135");
first.InfoUrl.Should().Be("https://passthepopcorn.me/torrents.php?id=148131&torrentid=452135");
first.PublishDate.Should().Be(DateTime.Parse("2016-10-18T23:40:59+0000").ToUniversalTime());

View File

@@ -66,7 +66,8 @@ namespace NzbDrone.Core.Test.Languages
new object[] { 51, Language.Afrikaans },
new object[] { 52, Language.Marathi },
new object[] { 53, Language.Tagalog },
new object[] { 54, Language.Urdu }
new object[] { 54, Language.Urdu },
new object[] { 55, Language.Romansh }
};
public static object[] ToIntCases =
@@ -127,7 +128,8 @@ namespace NzbDrone.Core.Test.Languages
new object[] { Language.Afrikaans, 51 },
new object[] { Language.Marathi, 52 },
new object[] { Language.Tagalog, 53 },
new object[] { Language.Urdu, 54 }
new object[] { Language.Urdu, 54 },
new object[] { Language.Romansh, 55 }
};
[Test]

View File

@@ -400,6 +400,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[TestCase("rum", "RO")]
[TestCase("per", "FA")]
[TestCase("ger", "DE")]
[TestCase("gsw", "DE")]
[TestCase("cze", "CS")]
[TestCase("ice", "IS")]
[TestCase("dut", "NL")]

View File

@@ -89,5 +89,14 @@ namespace NzbDrone.Core.Test.ParserTests
var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Urdu);
}
[TestCase("rm")]
[TestCase("roh")]
[TestCase("rm-CH")]
public void should_return_romansh(string isoCode)
{
var result = IsoLanguages.Find(isoCode);
result.Language.Should().Be(Language.Romansh);
}
}
}

View File

@@ -101,6 +101,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Title.2016.Hun/Ger/Ita.AAC.1080p.WebDL.x264-TKP21")]
[TestCase("Movie.Title.2016.1080p.10Bit.HEVC.WEBRip.HIN-ENG-GER.DD5.1.H.265")]
[TestCase("Movie.Title.2016.HU-IT-DE.AAC.1080p.WebDL.x264")]
[TestCase("Movie.Title.2016.SwissGerman.WEB-DL.h264-RlsGrp")]
public void should_parse_language_german(string postTitle)
{
var result = Parser.Parser.ParseMovieTitle(postTitle, true);
@@ -503,6 +504,15 @@ namespace NzbDrone.Core.Test.ParserTests
result.Should().Contain(Language.Urdu);
}
[TestCase("The.Movie.Name.2016.Romansh.WEB-DL.h264-RlsGrp")]
[TestCase("The.Movie.Name.2016.Rumantsch.WEB.DL.h264-RlsGrp")]
[TestCase("The Movie Name 2016 Romansch WEB DL h264-RlsGrp")]
public void should_parse_language_romansh(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);
result.Should().Contain(Language.Romansh);
}
[TestCase("Movie.Title.en.sub")]
[TestCase("Movie Title.eng.sub")]
[TestCase("Movie.Title.eng.forced.sub")]

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Organizer;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ModelEvent<NamingConfig>))]
public class NamingConfigCheck : HealthCheckBase, IProvideHealthCheck
{
private readonly INamingConfigService _namingConfigService;
public NamingConfigCheck(INamingConfigService namingConfigService, ILocalizationService localizationService)
: base(localizationService)
{
_namingConfigService = namingConfigService;
}
public override HealthCheck Check()
{
var namingConfig = _namingConfigService.GetConfig();
if (namingConfig.MovieFolderFormat.IsNotNullOrWhiteSpace())
{
var match = FileNameValidation.DeprecatedMovieFolderTokensRegex.Matches(namingConfig.MovieFolderFormat);
if (match.Any())
{
return new HealthCheck(
GetType(),
HealthCheckResult.Warning,
_localizationService.GetLocalizedString(
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage", new Dictionary<string, object>
{
{ "tokens", string.Join(", ", match.Select(c => c.Value).ToArray()) },
}));
}
}
return new HealthCheck(GetType());
}
}
}

View File

@@ -71,6 +71,8 @@ namespace NzbDrone.Core.ImportLists.TMDb
[FieldOption(Hint = "Tagalog")]
tl,
[FieldOption(Hint = "Urdu")]
ur
ur,
[FieldOption(Hint = "Raeto-Romance")]
rm
}
}

View File

@@ -33,5 +33,15 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
{
return new PassThePopcornParser(Settings, _logger);
}
public override HttpRequest GetDownloadRequest(string link)
{
var request = new HttpRequest(link);
request.Headers.Set("ApiUser", Settings.APIUser);
request.Headers.Set("ApiKey", Settings.APIKey);
return request;
}
}
}

View File

@@ -6,10 +6,8 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
public class PassThePopcornResponse
{
public string TotalResults { get; set; }
public List<PassThePopcornMovie> Movies { get; set; }
public IReadOnlyCollection<PassThePopcornMovie> Movies { get; set; }
public string Page { get; set; }
public string AuthKey { get; set; }
public string PassKey { get; set; }
}
public class PassThePopcornMovie
@@ -18,9 +16,9 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
public string Title { get; set; }
public string Year { get; set; }
public string Cover { get; set; }
public List<string> Tags { get; set; }
public IReadOnlyCollection<string> Tags { get; set; }
public string ImdbId { get; set; }
public List<PassThePopcornTorrent> Torrents { get; set; }
public IReadOnlyCollection<PassThePopcornTorrent> Torrents { get; set; }
}
public class PassThePopcornTorrent

View File

@@ -75,7 +75,6 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
flags |= IndexerFlags.G_Scene;
}
// Only add approved torrents
try
{
torrentInfos.Add(new PassThePopcornInfo
@@ -83,7 +82,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
Guid = $"PassThePopcorn-{id}",
Title = torrent.ReleaseName,
Size = long.Parse(torrent.Size),
DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey),
DownloadUrl = GetDownloadUrl(id),
InfoUrl = GetInfoUrl(result.GroupId, id),
Seeders = int.Parse(torrent.Seeders),
Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders),
@@ -118,16 +117,12 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
torrentInfos;
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
private string GetDownloadUrl(int torrentId, string authKey, string passKey)
private string GetDownloadUrl(int torrentId)
{
var url = new HttpUri(_settings.BaseUrl)
.CombinePath("/torrents.php")
.AddQueryParam("action", "download")
.AddQueryParam("id", torrentId)
.AddQueryParam("authkey", authKey)
.AddQueryParam("torrent_pass", passKey);
.AddQueryParam("id", torrentId);
return url.FullUri;
}
@@ -141,5 +136,7 @@ namespace NzbDrone.Core.Indexers.PassThePopcorn
return url.FullUri;
}
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
}
}

View File

@@ -125,6 +125,7 @@ namespace NzbDrone.Core.Languages
public static Language Marathi => new Language(52, "Marathi");
public static Language Tagalog => new Language(53, "Tagalog");
public static Language Urdu => new Language(54, "Urdu");
public static Language Romansh => new Language(55, "Romansh");
public static Language Any => new Language(-1, "Any");
public static Language Original => new Language(-2, "Original");
@@ -189,6 +190,7 @@ namespace NzbDrone.Core.Languages
Marathi,
Tagalog,
Urdu,
Romansh,
Any,
Original
};

View File

@@ -1471,7 +1471,7 @@
"NotificationsEmailSettingsServerHelpText": "Nom d'amfitrió o IP del servidor de correu electrònic",
"NotificationsJoinValidationInvalidDeviceId": "Els ID del dispositiu no són vàlids.",
"NotificationsSignalSettingsGroupIdPhoneNumber": "ID del grup / número de telèfon",
"ParseModalUnableToParse": "No s'ha pogut analitzar el títol proporcionat. Torna-ho a provar.",
"ParseModalUnableToParse": "No s'ha pogut analitzar el títol proporcionat. Torneu-ho a provar.",
"ReleaseSource": "Origen del llançament",
"RemotePathMappingsInfo": "Les mapes de camins remots poques vegades són necessaris, si {appName} i el vostre client de baixada són al mateix sistema, és millor que coincideixin amb els vostres camins. Per a més informació, vés al [wiki]({wikiLink}).",
"RemoveQueueItem": "Elimina - {sourceTitle}",
@@ -2014,5 +2014,9 @@
"MovieEditRootFolderHelpText": "Moure pel·lícules a la mateixa carpeta arrel es pot utilitzar per a canviar de nom carpetes de pel·lícules per a fer coincidir títols actualitzats o format de noms",
"UpdateMoviePath": "Actualitzar ruta de pel·lícula",
"ImportListsRadarrSettingsApiKeyHelpText": "Clau API de la instància de {appName} des de la qual importar (Radarr 3.0 o superior)",
"ImportListsRadarrSettingsFullUrlHelpText": "URL, incloent-hi port, de la instància de {appName} des de la qual importar (Radarr 3.0 o superior)"
"ImportListsRadarrSettingsFullUrlHelpText": "URL, incloent-hi port, de la instància de {appName} des de la qual importar (Radarr 3.0 o superior)",
"NotificationsAppriseSettingsIncludePoster": "Inclou el cartell",
"MovieFolderFormatHelpTextDeprecatedWarning": "Els tokens associats amb les propietats dels arxius de pel·lícula han quedat obsolets i no seran suportats en futures versions majors.",
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "El format de carpeta de pel·lícules no ha de contenir tokens obsolets relacionats amb els arxius: {tokens}",
"NotificationsAppriseSettingsIncludePosterHelpText": "Inclou el pòster al missatge"
}

View File

@@ -1323,5 +1323,20 @@
"IndexerSettingsApiPathHelpText": "Cesta k api, obvykle {url}",
"IndexerSettingsApiUrl": "URL API",
"IndexerSettingsApiUser": "Uživatel API",
"IndexerSettingsCategories": "Kategorie"
"IndexerSettingsCategories": "Kategorie",
"DownloadClientFloodSettingsPostImportTags": "Tagy po importu",
"DownloadClientDownloadStationValidationSharedFolderMissing": "Sdílená složka neexistuje",
"DownloadClientFreeboxNotLoggedIn": "Nepřihlášeno",
"DownloadClientFreeboxUnableToReachFreebox": "Nelze se připojit k Freebox API. Zkontrolujte nastavení 'Host', 'Port' nebo 'Použít SSL'. (Chyba: {exceptionMessage})",
"DownloadClientFloodSettingsStartOnAdd": "Spustit po přidání",
"DownloadClientFloodSettingsPostImportTagsHelpText": "Přiřadí tagy po dokončení importu.",
"DownloadClientFreeboxApiError": "Chyba z Freebox API: {errorDescription}",
"DownloadClientDownloadStationValidationApiVersion": "Verze API Download Station není podporována. Požadovaná verze je alespoň {requiredVersion}. Podporovaný rozsah je od {minVersion} do {maxVersion}",
"DownloadClientDownloadStationValidationFolderMissing": "Složka neexistuje",
"DownloadClientDownloadStationValidationNoDefaultDestination": "Chybí výchozí cílová složka",
"DownloadClientFloodSettingsRemovalInfo": "{appName} se postará o automatické mazání torrentů podle aktuálních kritérií seedování v Nastavení -> Indexery",
"DownloadClientFreeboxAuthenticationError": "Přihlášení k Freebox API se nezdařilo. Důvod: {errorDescription}",
"DownloadClientDownloadStationValidationFolderMissingDetail": "Složka '{downloadDir}' neexistuje, musí být vytvořená ručně ve Sdílené složce '{sharedFolder}'.",
"DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Přihlaste se do vaší DiskStation jako {username} a ručně to nastavte v nastavení DownloadStation pod BT/HTTP/FTP/NZB -> Umístění.",
"DownloadClientDownloadStationValidationSharedFolderMissingDetail": "DiskStation nemá sdílenou složku s názvem '{sharedFolder}', jste si jisti, že jste ji zadali správně?"
}

View File

@@ -1134,6 +1134,7 @@
"MovieFilesTotaling": "Movie Files Totaling",
"MovieFolderFormat": "Movie Folder Format",
"MovieFolderFormatHelpText": "Used when adding a new movie or moving movies via the movie editor",
"MovieFolderFormatHelpTextDeprecatedWarning": "Tokens associated with movie file properties have been deprecated and will no longer be supported in future major versions.",
"MovieFolderImportedTooltip": "Movie imported from movie folder",
"MovieFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Movie Title:30}`) or the beginning (e.g. `{Movie Title:-30}`) are both supported.",
"MovieGrabbedTooltip": "Movie grabbed from {indexer} and sent to {downloadClient}",
@@ -1173,6 +1174,7 @@
"MustNotContainHelpText": "The release will be rejected if it contains one or more of terms (case insensitive)",
"MyComputer": "My Computer",
"Name": "Name",
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "Movie Folder Format must not contain deprecated file related tokens: {tokens}",
"NamingSettings": "Naming Settings",
"NamingSettingsLoadError": "Unable to load Naming settings",
"Negate": "Negate",
@@ -1221,6 +1223,8 @@
"NotificationTriggersHelpText": "Select which events should trigger this notification",
"NotificationsAppriseSettingsConfigurationKey": "Apprise Configuration Key",
"NotificationsAppriseSettingsConfigurationKeyHelpText": "Configuration Key for the Persistent Storage Solution. Leave empty if Stateless URLs is used.",
"NotificationsAppriseSettingsIncludePoster": "Include Poster",
"NotificationsAppriseSettingsIncludePosterHelpText": "Include poster in message",
"NotificationsAppriseSettingsNotificationType": "Apprise Notification Type",
"NotificationsAppriseSettingsPasswordHelpText": "HTTP Basic Auth Password",
"NotificationsAppriseSettingsServerUrl": "Apprise Server URL",

View File

@@ -2014,5 +2014,9 @@
"ImportListsRadarrSettingsTagsHelpText": "Etiquetas de la instancia de la fuente de la que importar",
"ImportListsRadarrSettingsApiKeyHelpText": "Clave API de la instancia de {appName} desde la que importar (Radarr 3.0 o superior)",
"ImportListsRadarrSettingsFullUrlHelpText": "URL, incluyendo puerto, de la instancia de {appName} desde la que importar (Radarr 3.0 o superior)",
"EditMovieCollectionModalHeader": "Editar - {title}"
"EditMovieCollectionModalHeader": "Editar - {title}",
"NotificationsAppriseSettingsIncludePosterHelpText": "Incluir el póster en el mensaje",
"NotificationsAppriseSettingsIncludePoster": "Incluir póster",
"MovieFolderFormatHelpTextDeprecatedWarning": "Los tokens asociados con las propiedades de los archivos de película han quedado obsoletos y no serán soportados en futuras versiones mayores.",
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "El formato de carpeta de películas no debe contener tokens obsoletos relacionados con los archivos: {tokens}"
}

View File

@@ -828,7 +828,7 @@
"Status": "Tila",
"Studio": "Studio",
"Style": "Ulkoasu",
"AddNewMovieRootFolderHelpText": "Alikansio '{0}' luodaan automaattisesti",
"AddNewMovieRootFolderHelpText": "Alikansio \"{folder}\" luodaan automaattisesti.",
"Sunday": "Sunnuntai",
"Table": "Taulukko",
"TableOptions": "Taulukkonäkymän asetukset",
@@ -1289,7 +1289,7 @@
"ReleaseProfiles": "Julkaisuprofiilit",
"UnknownEventTooltip": "Tuntematon tapahtuma",
"DeleteSelectedMovieFilesHelpText": "Haluatko varmasti poistaa valitut elokuvatiedostot?",
"ApplyTagsHelpTextHowToApplyMovies": "Tunnisteiden käyt valituille elokuville:",
"ApplyTagsHelpTextHowToApplyMovies": "Miten tunnisteita käytetään valituille elokuville:",
"Popularity": "Suosio",
"IgnoreDownloadsHint": "Estää {appName}ia käsittelemästä näitä latauksia jatkossa.",
"NotificationsAppriseSettingsConfigurationKey": "Apprise-määritysavain",
@@ -2010,5 +2010,10 @@
"ImportListsRadarrSettingsQualityProfilesHelpText": "Lähdeinstanssin laatuprofiilit, joilla tuodaan.",
"ImportListsRadarrSettingsRootFoldersHelpText": "Lähdeinstanssin juurikansiot, joista tuodaan.",
"ImportListsRadarrSettingsTagsHelpText": "Lähdeinstanssin tunnisteet, joilla tuodaan.",
"EditMovieCollectionModalHeader": "Muokataan {title}"
"EditMovieCollectionModalHeader": "Muokataan {title}",
"MovieEditRootFolderHelpText": "Siirtämällä elokuvat niiden nykyiseen juurikansioon voidaan niiden kansioiden nimet päivittää vastaamaan päivittynyttä nimikettä tai nimeämiskaavaa.",
"ImportListsRadarrSettingsFullUrlHelpText": "Tuotavan {appName}-instanssin URL-osoite portti mukaan lukien (Radarr 3.0 tai uudempi).",
"ImportListsRadarrSettingsApiKeyHelpText": "Tuotavan {appName}-instanssin rajapinnan avain (Radarr 3.0 tai uudempi).",
"UpdateMoviePath": "Muuta elokuvan sijaintia",
"NotificationsAppriseSettingsIncludePoster": "Sisällytä juliste"
}

View File

@@ -907,7 +907,7 @@
"NoResultsFound": "Nincs találat",
"None": "Egyik sem",
"NoMoviesExist": "Nem találhatóak filmek, kezdésnek adjon hozzá egy új filmet vagy importáljon már meglévőket.",
"MoveMovieFoldersDontMoveFiles": " Ne, magam mozgatom át a fájlokat",
"MoveMovieFoldersDontMoveFiles": "Nem, magam helyezem át a fájlokat",
"NoListRecommendations": "Nincs elem a listában és az ajánlások között, kezdésnek adjon hozzá egy filmet, importáljon meglévőket, vagy adjon hozzá egy listát.",
"NoLinks": "Nincsenek Linkek",
"NoEventsFound": "Nem található események",

View File

@@ -602,7 +602,7 @@
"System": "Sistema",
"Sunday": "Domingo",
"SuggestTranslationChange": "Sugerir mudança de tradução",
"AddNewMovieRootFolderHelpText": "A subpasta \"{0}\" será criada automaticamente",
"AddNewMovieRootFolderHelpText": "A subpasta '{folder}' será criada automaticamente",
"Style": "Estilo",
"Studio": "Studio",
"Status": "Status",
@@ -717,7 +717,7 @@
"ReplaceIllegalCharacters": "Substituir Caracteres Ilegais",
"Replace": "Substituir",
"Reorder": "Reordenar",
"RenameMoviesHelpText": "O {appName} usará o nome de arquivo existente se a renomeação estiver deshabilitada",
"RenameMoviesHelpText": "O {appName} usará o nome de arquivo existente se a renomeação estiver desabilitada",
"RenameMovies": "Renomear filmes",
"RenameFiles": "Renomear Arquivos",
"Renamed": "Renomeado",
@@ -2014,5 +2014,9 @@
"ImportListsRadarrSettingsApiKeyHelpText": "Chave de API da instância {appName} para importar (Radarr 3.0 ou superior)",
"ImportListsRadarrSettingsFullUrlHelpText": "URL , incluindo porta da instância {appName} para importar (Radarr 3.0 ou superior)",
"UpdateMoviePath": "Atualizar Caminho do Filme",
"EditMovieCollectionModalHeader": "Editar - {title}"
"EditMovieCollectionModalHeader": "Editar - {title}",
"NotificationsAppriseSettingsIncludePosterHelpText": "Incluir pôster na mensagem",
"NotificationsAppriseSettingsIncludePoster": "Incluir pôster",
"MovieFolderFormatHelpTextDeprecatedWarning": "Os tokens associados às propriedades do arquivo de filme foram descontinuados e não terão mais suporte nas principais versões futuras.",
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "O formato de pasta do filme não deve conter tokens relacionados a arquivos obsoletos: {tokens}"
}

View File

@@ -259,7 +259,7 @@
"InCinemasDate": "Даты в кинотеатрах",
"Indexer": "Индексатор",
"IndexerLongTermStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов",
"IndexerLongTermStatusCheckSingleClientMessage": "Все индексаторы недоступны из-за ошибок за последние 6 часов: {indexerNames}",
"IndexerLongTermStatusCheckSingleClientMessage": "Индексаторы недоступны из-за ошибок за последние 6 часов: {indexerNames}",
"IndexerPriority": "Приоритет индексатора",
"IndexerStatusCheckAllClientMessage": "Все индексаторы недоступны из-за ошибок",
"InstallLatest": "Установить последнюю",
@@ -833,7 +833,7 @@
"TimeFormat": "Формат времени",
"Time": "Время",
"ThisCannotBeCancelled": "Это действие нельзя отменить после запуска без отключения всех ваших индексаторов.",
"TheLogLevelDefault": "Уровень журналирования по умолчанию установлен на 'Информация' и может быть изменён в [Общих настройках](/settings/general)",
"TheLogLevelDefault": "Уровень журналирования по умолчанию установлен на 'Отладка' и может быть изменён в [Общих настройках](/settings/general)",
"TestAllLists": "Тестировать все листы",
"TestAllIndexers": "Тестировать все индексаторы",
"TestAllClients": "Тестировать все клиенты",
@@ -2014,5 +2014,9 @@
"MovieEditRootFolderHelpText": "Перемещение фильмов в ту же корневую директорию может быть полезно для переименования папок для соответствия обновленным названиям или форматам именования",
"UpdateMoviePath": "Обновить путь к фильму",
"ImportListsRadarrSettingsApiKeyHelpText": "API ключ экземпляра {appName}, откуда импортируются данные (Radarr 3.0 или выше)",
"ImportListsRadarrSettingsFullUrlHelpText": "URL {appName}, включая порт, откуда импортируются данные (Radarr 3.0 или выше)"
"ImportListsRadarrSettingsFullUrlHelpText": "URL {appName}, включая порт, откуда импортируются данные (Radarr 3.0 или выше)",
"NotificationsAppriseSettingsIncludePoster": "Добавить постер",
"MovieFolderFormatHelpTextDeprecatedWarning": "Токены, связанные с параметрами файла фильма, устарели и не будут поддерживаться в грядущих мажорных версиях.",
"NotificationsAppriseSettingsIncludePosterHelpText": "Добавлять постер в сообщение",
"NamingConfigMovieFolderFormatDeprecatedHealthCheckMessage": "Формат папки фильма не должен содержать устаревшие токены, связанные с файлом фильма: {tokens}"
}

View File

@@ -2014,5 +2014,6 @@
"ImportListsRadarrSettingsFullUrlHelpText": "İçe aktarmak istediğiniz {appName} örneğinin bağlantı noktası da dahil olmak üzere URL'si (Radarr 3.0 veya üzeri)",
"MovieEditRootFolderHelpText": "Filmleri aynı kök klasöre taşımak, film klasörlerinin güncellenen başlığa veya adlandırma biçimine uyacak şekilde yeniden adlandırılmasında kullanılabilir",
"UpdateMoviePath": "Film Yolunu Güncelle",
"EditMovieCollectionModalHeader": "Düzenle - {title}"
"EditMovieCollectionModalHeader": "Düzenle - {title}",
"NotificationsAppriseSettingsIncludePoster": "Posteri Dahil Et"
}

View File

@@ -2007,5 +2007,6 @@
"ImportListsRadarrSettingsRootFoldersHelpText": "Кореневі теки вихідного екземпляра для імпорту",
"ImportListsRadarrSettingsTagsHelpText": "Теги з вихідного екземпляра для імпорту",
"ImportListsRadarrSettingsFullUrl": "Повний URL-адрес",
"EditMovieCollectionModalHeader": "Редагувати - {title}"
"EditMovieCollectionModalHeader": "Редагувати - {title}",
"NotificationsAppriseSettingsIncludePoster": "Включити постер"
}

View File

@@ -5,5 +5,14 @@
"Analytics": "分析",
"Username": "用户名",
"Activity": "111",
"KeyboardShortcutsConfirmModal": "中文"
"KeyboardShortcutsConfirmModal": "中文",
"Backup": "备份",
"Uptime": "运行时间",
"YouCanAlsoSearch": "你可以通过TMDB或IMDB的ID进行搜索如:`tmdb:71663`",
"UseProxy": "使用代理",
"YesCancel": "确认 ,取消",
"Yesterday": "昨天",
"Updates": "更新",
"Warn": "警告",
"BackupNow": "立即备份"
}

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
namespace NzbDrone.Core.Notifications.Apprise
@@ -20,47 +22,47 @@ namespace NzbDrone.Core.Notifications.Apprise
public override void OnGrab(GrabMessage grabMessage)
{
_proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage.Message, Settings);
_proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage.Message, GetPosterUrl(grabMessage.Movie), Settings);
}
public override void OnDownload(DownloadMessage message)
{
_proxy.SendNotification(MOVIE_DOWNLOADED_TITLE, message.Message, Settings);
_proxy.SendNotification(message.OldMovieFiles.Any() ? MOVIE_UPGRADED_TITLE : MOVIE_DOWNLOADED_TITLE, message.Message, GetPosterUrl(message.Movie), Settings);
}
public override void OnMovieAdded(Movie movie)
{
_proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} added to library", Settings);
_proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} ({movie.Year}) added to library", GetPosterUrl(movie), Settings);
}
public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage)
{
_proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, Settings);
_proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, GetPosterUrl(deleteMessage.Movie), Settings);
}
public override void OnMovieDelete(MovieDeleteMessage deleteMessage)
{
_proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, Settings);
_proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, GetPosterUrl(deleteMessage.Movie), Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, null, Settings);
}
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
{
_proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
_proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", null, Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, null, Settings);
}
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
{
_proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings);
_proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, GetPosterUrl(message.Movie), Settings);
}
public override ValidationResult Test()
@@ -71,5 +73,10 @@ namespace NzbDrone.Core.Notifications.Apprise
return new ValidationResult(failures);
}
private static string GetPosterUrl(Movie movie)
{
return movie?.MovieMetadata?.Value?.Images?.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Poster)?.RemoteUrl;
}
}
}

View File

@@ -8,6 +8,8 @@ namespace NzbDrone.Core.Notifications.Apprise
public string Body { get; set; }
public string Attachment { get; set; }
public AppriseNotificationType Type { get; set; }
public string Tag { get; set; }

View File

@@ -13,7 +13,7 @@ namespace NzbDrone.Core.Notifications.Apprise
{
public interface IAppriseProxy
{
void SendNotification(string title, string message, AppriseSettings settings);
void SendNotification(string title, string message, string posterUrl, AppriseSettings settings);
ValidationFailure Test(AppriseSettings settings);
}
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Notifications.Apprise
_logger = logger;
}
public void SendNotification(string title, string message, AppriseSettings settings)
public void SendNotification(string title, string message, string posterUrl, AppriseSettings settings)
{
var payload = new ApprisePayload
{
@@ -61,6 +61,11 @@ namespace NzbDrone.Core.Notifications.Apprise
payload.Tag = settings.Tags.Join(",");
}
if (settings.IncludePoster && posterUrl.IsNotNullOrWhiteSpace())
{
payload.Attachment = posterUrl;
}
if (settings.AuthUsername.IsNotNullOrWhiteSpace() || settings.AuthPassword.IsNotNullOrWhiteSpace())
{
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.AuthUsername, settings.AuthPassword);
@@ -86,10 +91,11 @@ namespace NzbDrone.Core.Notifications.Apprise
{
const string title = "Radarr - Test Notification";
const string body = "Success! You have properly configured your apprise notification settings.";
const string posterUrl = "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/128.png";
try
{
SendNotification(title, body, settings);
SendNotification(title, body, posterUrl, settings);
}
catch (AppriseException ex) when (ex.InnerException is HttpException httpException)
{

View File

@@ -59,10 +59,13 @@ namespace NzbDrone.Core.Notifications.Apprise
[FieldDefinition(5, Label = "NotificationsAppriseSettingsTags", Type = FieldType.Tag, HelpText = "NotificationsAppriseSettingsTagsHelpText")]
public IEnumerable<string> Tags { get; set; }
[FieldDefinition(6, Label = "Username", Type = FieldType.Textbox, HelpText = "NotificationsAppriseSettingsUsernameHelpText", Privacy = PrivacyLevel.UserName)]
[FieldDefinition(6, Label = "NotificationsAppriseSettingsIncludePoster", Type = FieldType.Checkbox, HelpText = "NotificationsAppriseSettingsIncludePosterHelpText")]
public bool IncludePoster { get; set; }
[FieldDefinition(7, Label = "Username", Type = FieldType.Textbox, HelpText = "NotificationsAppriseSettingsUsernameHelpText", Privacy = PrivacyLevel.UserName)]
public string AuthUsername { get; set; }
[FieldDefinition(7, Label = "Password", Type = FieldType.Password, HelpText = "NotificationsAppriseSettingsPasswordHelpText", Privacy = PrivacyLevel.Password)]
[FieldDefinition(8, Label = "Password", Type = FieldType.Password, HelpText = "NotificationsAppriseSettingsPasswordHelpText", Privacy = PrivacyLevel.Password)]
public string AuthPassword { get; set; }
public override NzbDroneValidationResult Validate()

View File

@@ -70,6 +70,7 @@ namespace NzbDrone.Core.Organizer
{ "geo", "kat" },
{ "ger", "deu" },
{ "gre", "ell" },
{ "gsw", "deu" },
{ "ice", "isl" },
{ "mac", "mkd" },
{ "mao", "mri" },
@@ -171,16 +172,17 @@ namespace NzbDrone.Core.Organizer
namingConfig = _namingConfigService.GetConfig();
}
var movieFile = movie.MovieFile;
var pattern = namingConfig.MovieFolderFormat;
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
var multipleTokens = TitleRegex.Matches(pattern).Count > 1;
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
AddMovieTokens(tokenHandlers, movie);
AddReleaseDateTokens(tokenHandlers, movie.Year);
AddIdTokens(tokenHandlers, movie);
var movieFile = movie.MovieFile;
if (movie.MovieFile != null)
{
AddQualityTokens(tokenHandlers, movie, movieFile);

View File

@@ -19,14 +19,13 @@ namespace NzbDrone.Core.Organizer
private static MovieFile _movieFile;
private static Movie _movie;
private static MovieMetadata _movieMetadata;
private static List<CustomFormat> _customFormats;
public FileNameSampleService(IBuildFileNames buildFileNames)
{
_buildFileNames = buildFileNames;
var mediaInfo = new MediaInfoModel()
var mediaInfo = new MediaInfoModel
{
VideoFormat = "AVC",
VideoBitDepth = 10,
@@ -50,23 +49,19 @@ namespace NzbDrone.Core.Organizer
Edition = "Ultimate extended edition",
};
_movieMetadata = new MovieMetadata
{
Title = "The Movie: Title",
OriginalTitle = "The Original Movie Title",
CollectionTitle = "The Movie Collection",
CollectionTmdbId = 123654,
Certification = "R",
Year = 2010,
ImdbId = "tt0066921",
TmdbId = 345691
};
_movie = new Movie
{
MovieFile = _movieFile,
MovieFileId = 1,
MovieMetadata = _movieMetadata,
MovieMetadata = new MovieMetadata
{
Title = "The Movie: Title",
OriginalTitle = "The Original Movie Title",
CollectionTitle = "The Movie Collection",
CollectionTmdbId = 123654,
Certification = "R",
Year = 2010,
ImdbId = "tt0066921",
TmdbId = 345691
},
MovieMetadataId = 1
};

View File

@@ -9,6 +9,9 @@ namespace NzbDrone.Core.Organizer
{
public static class FileNameValidation
{
public static readonly Regex DeprecatedMovieFolderTokensRegex = new (@"(\{[- ._\[\(]?(?:Original[- ._](?:Title|Filename)|Release[- ._]Group|Edition[- ._]Tags|Quality[- ._](?:Full|Title|Proper|Real)|MediaInfo[- ._](?:Video|VideoCodec|VideoBitDepth|Audio|AudioCodec|AudioChannels|AudioLanguages|AudioLanguagesAll|SubtitleLanguages|SubtitleLanguagesAll|3D|Simple|Full|VideoDynamicRange|VideoDynamicRangeType))[- ._\]\)]?\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static readonly Regex OriginalTokenRegex = new (@"(\{Original[- ._](?:Title|Filename)\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase);

View File

@@ -9,6 +9,8 @@ namespace NzbDrone.Core.Organizer
public class NamingConfigRepository : BasicRepository<NamingConfig>, INamingConfigRepository
{
protected override bool PublishModelEvents => true;
public NamingConfigRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{

View File

@@ -63,7 +63,8 @@ namespace NzbDrone.Core.Parser
new IsoLanguage("af", "", "afr", "Afrikaans", Language.Afrikaans),
new IsoLanguage("mr", "", "mar", "Marathi", Language.Marathi),
new IsoLanguage("tl", "", "tgl", "Tagalog", Language.Tagalog),
new IsoLanguage("ur", "", "urd", "Urdu", Language.Urdu)
new IsoLanguage("ur", "", "urd", "Urdu", Language.Urdu),
new IsoLanguage("rm", "", "roh", "Romansh", Language.Romansh)
};
private static readonly Dictionary<string, Language> AlternateIsoCodeMappings = new ()

View File

@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_|^)(?<english>\beng\b)|
(?<italian>\b(?:ita|italian)\b)|
(?<german>german\b|videomann|ger[. ]dub|\bger\b)|
(?<german>(?:swiss)?german\b|videomann|ger[. ]dub|\bger\b)|
(?<flemish>flemish)|
(?<bulgarian>bgaudio)|
(?<romanian>rodubbed)|
@@ -38,6 +38,7 @@ namespace NzbDrone.Core.Parser
(?<japanese>\bJAP\b)|
(?<korean>\bKOR\b)|
(?<urdu>\burdu\b)|
(?<romansh>\b(?:romansh|rumantsch|romansch)\b)|
(?<original>\b(?:orig|original)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
@@ -426,6 +427,11 @@ namespace NzbDrone.Core.Parser
languages.Add(Language.Urdu);
}
if (match.Groups["romansh"].Success)
{
languages.Add(Language.Romansh);
}
if (match.Groups["original"].Success)
{
languages.Add(Language.Original);

View File

@@ -21,7 +21,7 @@
<PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
<PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
<PackageReference Include="FluentValidation" Version="9.5.4" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />

1714
yarn.lock

File diff suppressed because it is too large Load Diff