mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-08 21:21:47 +02:00
Merge branch 'v5-develop' into feature/standardize-trakt-settings
This commit is contained in:
commit
de48f79ce7
51 changed files with 1323 additions and 213 deletions
|
|
@ -6,7 +6,6 @@ import AppSectionState, {
|
|||
AppSectionSchemaState,
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
|
||||
|
|
@ -101,7 +100,6 @@ export interface ImportListExclusionsSettingsAppState
|
|||
}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
|
||||
interface SettingsAppState {
|
||||
autoTaggings: AutoTaggingAppState;
|
||||
|
|
@ -118,7 +116,6 @@ interface SettingsAppState {
|
|||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexerOptions: IndexerOptionsAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import { useLanguages } from 'Language/useLanguages';
|
||||
import FilterBuilderRowValue, {
|
||||
FilterBuilderRowValueProps,
|
||||
} from './FilterBuilderRowValue';
|
||||
|
|
@ -13,7 +12,7 @@ type LanguageFilterBuilderRowValueProps<T> = Omit<
|
|||
function LanguageFilterBuilderRowValue<T>(
|
||||
props: LanguageFilterBuilderRowValueProps<T>
|
||||
) {
|
||||
const { items } = useSelector(createLanguagesSelector());
|
||||
const { data: items = [] } = useLanguages();
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Language from 'Language/Language';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import { useFilteredLanguages } from 'Language/useLanguages';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput, {
|
||||
EnhancedSelectInputValue,
|
||||
|
|
@ -31,13 +30,11 @@ export default function LanguageSelectInput({
|
|||
onChange,
|
||||
...otherProps
|
||||
}: LanguageSelectInputProps) {
|
||||
const { items } = useSelector(
|
||||
createLanguagesSelector({
|
||||
Any: true,
|
||||
Original: true,
|
||||
Unknown: true,
|
||||
})
|
||||
);
|
||||
const { data: items = [] } = useFilteredLanguages({
|
||||
includeAny: true,
|
||||
includeOriginal: true,
|
||||
includeUnknown: true,
|
||||
});
|
||||
|
||||
const values = useMemo(() => {
|
||||
const result: EnhancedSelectInputValue<number | string>[] = items.map(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import useLanguageName from 'Language/useLanguageName';
|
||||
import MediaInfoProps from 'typings/MediaInfo';
|
||||
import formatBitrate from 'Utilities/Number/formatBitrate';
|
||||
import getEntries from 'Utilities/Object/getEntries';
|
||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function MediaInfo(props: MediaInfoProps) {
|
||||
const getLanguageName = useLanguageName();
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
{getEntries(props).map(([key, value]) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||
import useLanguageName from 'Language/useLanguageName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { useEpisodeFile } from './EpisodeFileProvider';
|
||||
|
||||
function formatLanguages(languages: string[] | undefined) {
|
||||
function formatLanguages(
|
||||
languages: string[] | undefined,
|
||||
getLanguageName: (code: string) => string
|
||||
) {
|
||||
if (!languages) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -43,6 +46,7 @@ interface MediaInfoProps {
|
|||
}
|
||||
|
||||
function MediaInfo({ episodeFileId, type }: MediaInfoProps) {
|
||||
const getLanguageName = useLanguageName();
|
||||
const episodeFile = useEpisodeFile(episodeFileId);
|
||||
|
||||
if (!episodeFile?.mediaInfo) {
|
||||
|
|
@ -76,11 +80,17 @@ function MediaInfo({ episodeFileId, type }: MediaInfoProps) {
|
|||
}
|
||||
|
||||
if (type === 'audioLanguages') {
|
||||
return formatLanguages(audioStreams.map(({ language }) => language));
|
||||
return formatLanguages(
|
||||
audioStreams.map(({ language }) => language),
|
||||
getLanguageName
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'subtitles') {
|
||||
return formatLanguages(subtitleStreams.map(({ language }) => language));
|
||||
return formatLanguages(
|
||||
subtitleStreams.map(({ language }) => language),
|
||||
getLanguageName
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'video') {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import AppState from 'App/State/AppState';
|
|||
import { useTranslations } from 'App/useTranslations';
|
||||
import useCommands from 'Commands/useCommands';
|
||||
import useCustomFilters from 'Filters/useCustomFilters';
|
||||
import { useInitializeLanguage } from 'Language/useLanguageName';
|
||||
import { useLanguages } from 'Language/useLanguages';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import { useQualityProfiles } from 'Settings/Profiles/Quality/useQualityProfiles';
|
||||
import { useUiSettings } from 'Settings/UI/useUiSettings';
|
||||
|
|
@ -12,7 +14,6 @@ import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
|||
import {
|
||||
fetchImportLists,
|
||||
fetchIndexerFlags,
|
||||
fetchLanguages,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import useSystemStatus from 'System/Status/useSystemStatus';
|
||||
import useTags from 'Tags/useTags';
|
||||
|
|
@ -26,6 +27,7 @@ const createErrorsSelector = ({
|
|||
uiSettingsError,
|
||||
seriesError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
}: {
|
||||
customFiltersError: ApiError | null;
|
||||
systemStatusError: ApiError | null;
|
||||
|
|
@ -34,12 +36,12 @@ const createErrorsSelector = ({
|
|||
uiSettingsError: ApiError | null;
|
||||
seriesError: ApiError | null;
|
||||
qualityProfilesError: ApiError | null;
|
||||
languagesError: ApiError | null;
|
||||
}) =>
|
||||
createSelector(
|
||||
(state: AppState) => state.settings.languages.error,
|
||||
(state: AppState) => state.settings.importLists.error,
|
||||
(state: AppState) => state.settings.indexerFlags.error,
|
||||
(languagesError, importListsError, indexerFlagsError) => {
|
||||
(importListsError, indexerFlagsError) => {
|
||||
const hasError = !!(
|
||||
customFiltersError ||
|
||||
seriesError ||
|
||||
|
|
@ -76,11 +78,12 @@ const useAppPage = () => {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
useCommands();
|
||||
useInitializeLanguage();
|
||||
|
||||
const { isFetched: isCustomFiltersFetched, error: customFiltersError } =
|
||||
useCustomFilters();
|
||||
|
||||
const { isSuccess: isSeriesFetched, error: seriesError } = useSeries();
|
||||
const { isFetched: isSeriesFetched, error: seriesError } = useSeries();
|
||||
|
||||
const { isFetched: isSystemStatusFetched, error: systemStatusError } =
|
||||
useSystemStatus();
|
||||
|
|
@ -96,9 +99,11 @@ const useAppPage = () => {
|
|||
const { isFetched: isQualityProfilesFetched, error: qualityProfilesError } =
|
||||
useQualityProfiles();
|
||||
|
||||
const { isFetched: isLanguagesFetched, error: languagesError } =
|
||||
useLanguages();
|
||||
|
||||
const isAppStatePopulated = useSelector(
|
||||
(state: AppState) =>
|
||||
state.settings.languages.isPopulated &&
|
||||
state.settings.importLists.isPopulated &&
|
||||
state.settings.indexerFlags.isPopulated
|
||||
);
|
||||
|
|
@ -111,7 +116,8 @@ const useAppPage = () => {
|
|||
isTagsFetched &&
|
||||
isTranslationsFetched &&
|
||||
isUiSettingsFetched &&
|
||||
isQualityProfilesFetched;
|
||||
isQualityProfilesFetched &&
|
||||
isLanguagesFetched;
|
||||
|
||||
const { hasError, errors } = useSelector(
|
||||
createErrorsSelector({
|
||||
|
|
@ -122,6 +128,7 @@ const useAppPage = () => {
|
|||
translationsError,
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -140,7 +147,6 @@ const useAppPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCustomFilters());
|
||||
dispatch(fetchLanguages());
|
||||
dispatch(fetchImportLists());
|
||||
dispatch(fetchIndexerFlags());
|
||||
}, [dispatch]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
|
|
@ -13,7 +12,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import { useFilteredLanguages } from 'Language/useLanguages';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectLanguageModalContent.css';
|
||||
|
||||
|
|
@ -27,12 +26,15 @@ interface SelectLanguageModalContentProps {
|
|||
function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
|
||||
const { modalTitle, onLanguagesSelect, onModalClose } = props;
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
createLanguagesSelector({
|
||||
Any: true,
|
||||
Original: true,
|
||||
})
|
||||
);
|
||||
const {
|
||||
data: items = [],
|
||||
isFetching,
|
||||
isFetched: isPopulated,
|
||||
error,
|
||||
} = useFilteredLanguages({
|
||||
includeAny: true,
|
||||
includeOriginal: true,
|
||||
});
|
||||
|
||||
const [languageIds, setLanguageIds] = useState(props.languageIds);
|
||||
|
||||
|
|
|
|||
58
frontend/src/Language/useLanguageName.ts
Normal file
58
frontend/src/Language/useLanguageName.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import moment from 'moment';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
|
||||
interface LanguageResponse {
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
function getDisplayName(code: string) {
|
||||
return Intl.DisplayNames
|
||||
? new Intl.DisplayNames([code], { type: 'language' })
|
||||
: null;
|
||||
}
|
||||
|
||||
const useLanguage = () => {
|
||||
return useApiQuery<LanguageResponse>({
|
||||
path: '/localization/language',
|
||||
queryOptions: {
|
||||
staleTime: Infinity,
|
||||
gcTime: Infinity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useInitializeLanguage = () => {
|
||||
const { data } = useLanguage();
|
||||
|
||||
useEffect(() => {
|
||||
moment.locale(data?.identifier);
|
||||
}, [data]);
|
||||
};
|
||||
|
||||
const useLanguageName = () => {
|
||||
const { data } = useLanguage();
|
||||
|
||||
const getLanguageName = useCallback(
|
||||
(code: string): string => {
|
||||
const languageNames = data?.identifier
|
||||
? getDisplayName(data.identifier)
|
||||
: getDisplayName('en');
|
||||
|
||||
if (!languageNames) {
|
||||
return code;
|
||||
}
|
||||
|
||||
try {
|
||||
return languageNames.of(code) ?? code;
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return getLanguageName;
|
||||
};
|
||||
|
||||
export default useLanguageName;
|
||||
65
frontend/src/Language/useLanguages.ts
Normal file
65
frontend/src/Language/useLanguages.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useMemo } from 'react';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import Language from 'Language/Language';
|
||||
|
||||
interface LanguageFilter {
|
||||
[key: string]: boolean | undefined;
|
||||
includeAny: boolean;
|
||||
includeOriginal?: boolean;
|
||||
includeUnknown?: boolean;
|
||||
}
|
||||
|
||||
const PATH = '/language';
|
||||
|
||||
export const useLanguages = () => {
|
||||
return useApiQuery<Language[]>({
|
||||
path: PATH,
|
||||
queryOptions: {
|
||||
gcTime: Infinity,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useFilteredLanguages = (
|
||||
excludeLanguages: LanguageFilter = { includeAny: true }
|
||||
) => {
|
||||
const { data, isFetching, isFetched, error } = useLanguages();
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
return data.filter((lang) => !excludeLanguages[lang.name]);
|
||||
}, [data, excludeLanguages]);
|
||||
|
||||
return {
|
||||
data: filteredItems,
|
||||
isFetching,
|
||||
isFetched,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useLanguageById = (id: number | undefined) => {
|
||||
const { data } = useLanguages();
|
||||
|
||||
return useMemo(() => {
|
||||
if (id === undefined || !data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return data.find((language) => language.id === id);
|
||||
}, [data, id]);
|
||||
};
|
||||
|
||||
export const useLanguageByName = (name: string | undefined) => {
|
||||
const { data } = useLanguages();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!name || !data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return data.find((language) => language.name === name);
|
||||
}, [data, name]);
|
||||
};
|
||||
|
|
@ -39,8 +39,17 @@ function EditReleaseProfileModalContent({
|
|||
saveProvider,
|
||||
} = useManageReleaseProfile(id ?? 0);
|
||||
|
||||
const { name, enabled, required, ignored, indexerIds, tags, excludedTags } =
|
||||
item;
|
||||
const {
|
||||
name,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
airDateRestriction,
|
||||
airDateGracePeriod,
|
||||
indexerIds,
|
||||
tags,
|
||||
excludedTags,
|
||||
} = item;
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
|
|
@ -131,6 +140,33 @@ function EditReleaseProfileModalContent({
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AirDateRestriction')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
{...airDateRestriction}
|
||||
type={inputTypes.CHECK}
|
||||
name="airDateRestriction"
|
||||
helpText={translate('AirDateRestrictionHelpText')}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{airDateRestriction.value ? (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AirDateGracePeriod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
{...airDateGracePeriod}
|
||||
type={inputTypes.NUMBER}
|
||||
unit="days"
|
||||
name="airDateGracePeriod"
|
||||
helpText={translate('AirDateGracePeriodHelpText')}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Indexer')}</FormLabel>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export interface ReleaseProfileModel extends ModelBase {
|
|||
enabled: boolean;
|
||||
required: string[];
|
||||
ignored: string[];
|
||||
airDateRestriction: boolean;
|
||||
airDateGracePeriod: number;
|
||||
indexerIds: number[];
|
||||
tags: number[];
|
||||
excludedTags: number[];
|
||||
|
|
@ -23,6 +25,8 @@ const NEW_RELEASE_PROFILE: ReleaseProfileModel = {
|
|||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
airDateRestriction: false,
|
||||
airDateGracePeriod: 0,
|
||||
indexerIds: [],
|
||||
tags: [],
|
||||
excludedTags: [],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
|
|
@ -11,8 +10,8 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { useFilteredLanguages } from 'Language/useLanguages';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import themes from 'Styles/Themes';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import timeZoneOptions from 'Utilities/Date/timeZoneOptions';
|
||||
|
|
@ -63,17 +62,15 @@ export const timeFormatOptions: EnhancedSelectInputValue<string>[] = [
|
|||
|
||||
function UISettings() {
|
||||
const {
|
||||
items,
|
||||
data: languageItems = [],
|
||||
isFetching: isLanguagesFetching,
|
||||
isPopulated: isLanguagesPopulated,
|
||||
isFetched: isLanguagesPopulated,
|
||||
error: languagesError,
|
||||
} = useSelector(
|
||||
createLanguagesSelector({
|
||||
Any: true,
|
||||
Original: true,
|
||||
Unknown: true,
|
||||
})
|
||||
);
|
||||
} = useFilteredLanguages({
|
||||
includeAny: true,
|
||||
includeOriginal: true,
|
||||
includeUnknown: true,
|
||||
});
|
||||
|
||||
const {
|
||||
isFetching: isSettingsFetching,
|
||||
|
|
@ -94,13 +91,13 @@ function UISettings() {
|
|||
const error = languagesError || settingsError;
|
||||
|
||||
const languages = useMemo(() => {
|
||||
return items.map((item) => {
|
||||
return languageItems.map((item) => {
|
||||
return {
|
||||
key: item.id,
|
||||
value: item.name,
|
||||
};
|
||||
});
|
||||
}, [items]);
|
||||
}, [languageItems]);
|
||||
|
||||
const themeOptions = Object.keys(themes).map((theme) => ({
|
||||
key: theme,
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.languages';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_LANGUAGES = 'settings/languages/fetchLanguages';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchLanguages = createThunk(FETCH_LANGUAGES);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_LANGUAGES]: createFetchHandler(section, '/language')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -14,7 +14,6 @@ import importLists from './Settings/importLists';
|
|||
import indexerFlags from './Settings/indexerFlags';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
import languages from './Settings/languages';
|
||||
|
||||
export * from './Settings/autoTaggingSpecifications';
|
||||
export * from './Settings/autoTaggings';
|
||||
|
|
@ -30,7 +29,6 @@ export * from './Settings/importListExclusions';
|
|||
export * from './Settings/indexerFlags';
|
||||
export * from './Settings/indexerOptions';
|
||||
export * from './Settings/indexers';
|
||||
export * from './Settings/languages';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
|
@ -55,8 +53,7 @@ export const defaultState = {
|
|||
importListOptions: importListOptions.defaultState,
|
||||
indexerFlags: indexerFlags.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
languages: languages.defaultState
|
||||
indexers: indexers.defaultState
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
|
|
@ -80,8 +77,7 @@ export const actionHandlers = handleThunks({
|
|||
...importListOptions.actionHandlers,
|
||||
...indexerFlags.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...languages.actionHandlers
|
||||
...indexers.actionHandlers
|
||||
});
|
||||
|
||||
//
|
||||
|
|
@ -101,7 +97,6 @@ export const reducers = createHandleActions({
|
|||
...importListOptions.reducers,
|
||||
...indexerFlags.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...languages.reducers
|
||||
...indexers.reducers
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
interface LanguageFilter {
|
||||
[key: string]: boolean | undefined;
|
||||
Any: boolean;
|
||||
Original?: boolean;
|
||||
Unknown?: boolean;
|
||||
}
|
||||
|
||||
function createLanguagesSelector(
|
||||
excludeLanguages: LanguageFilter = { Any: true }
|
||||
) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.languages,
|
||||
(languages) => {
|
||||
const { isFetching, isPopulated, error, items } = languages;
|
||||
|
||||
const filteredLanguages = items.filter(
|
||||
(lang) => !excludeLanguages[lang.name]
|
||||
);
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items: filteredLanguages,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createLanguagesSelector;
|
||||
|
|
@ -74,7 +74,7 @@ function getRelativeDate({
|
|||
|
||||
if (isInNextWeek(date)) {
|
||||
const dateTime = convertToTimezone(date, timeZone);
|
||||
const day = dateTime.format('dddd');
|
||||
const day = getDayOfWeek(dateTime.day());
|
||||
|
||||
return includeTime ? translate('DayOfWeekAt', { day, time }) : day;
|
||||
}
|
||||
|
|
@ -88,3 +88,24 @@ function getRelativeDate({
|
|||
}
|
||||
|
||||
export default getRelativeDate;
|
||||
|
||||
function getDayOfWeek(dayNumber: number) {
|
||||
switch (dayNumber) {
|
||||
case 0:
|
||||
return translate('Sunday');
|
||||
case 1:
|
||||
return translate('Monday');
|
||||
case 2:
|
||||
return translate('Tuesday');
|
||||
case 3:
|
||||
return translate('Wednesday');
|
||||
case 4:
|
||||
return translate('Thursday');
|
||||
case 5:
|
||||
return translate('Friday');
|
||||
case 6:
|
||||
return translate('Saturday');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
|
||||
interface LanguageResponse {
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
return createAjaxRequest({
|
||||
global: false,
|
||||
dataType: 'json',
|
||||
url: '/localization/language',
|
||||
}).request;
|
||||
}
|
||||
|
||||
function getDisplayName(code: string) {
|
||||
return Intl.DisplayNames
|
||||
? new Intl.DisplayNames([code], { type: 'language' })
|
||||
: null;
|
||||
}
|
||||
|
||||
let languageNames = getDisplayName('en');
|
||||
|
||||
getLanguage().then((data: LanguageResponse) => {
|
||||
const names = getDisplayName(data.identifier);
|
||||
|
||||
if (names) {
|
||||
languageNames = names;
|
||||
}
|
||||
});
|
||||
|
||||
export default function getLanguageName(code: string) {
|
||||
if (!languageNames) {
|
||||
return code;
|
||||
}
|
||||
|
||||
try {
|
||||
return languageNames.of(code) ?? code;
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "10.0.102"
|
||||
"version": "10.0.103"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ dotnet clean $slnFile -c Release
|
|||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 10.1.0 Swashbuckle.AspNetCore.Cli
|
||||
dotnet tool install --version 10.1.2 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
# Remove the openapi.json file so we can check if it was created
|
||||
rm $outputFile
|
||||
|
|
|
|||
|
|
@ -6,20 +6,20 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="IPAddressRange" Version="6.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="NLog" Version="5.5.1" />
|
||||
<PackageReference Include="NLog.Layouts.ClefJsonLayout" Version="1.0.4" />
|
||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
|
||||
<PackageReference Include="Sentry" Version="5.16.2" />
|
||||
<PackageReference Include="Sentry" Version="5.16.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.5" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="2.0.2" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.2" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="6.0.0-preview.5.21301.5" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.2" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="EnsureThat\Resources\ExceptionMessages.Designer.cs">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AirDateSpecificationFixture : CoreTest<AirDateSpecification>
|
||||
{
|
||||
private RemoteEpisode _remoteEpisode;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_remoteEpisode = new RemoteEpisode
|
||||
{
|
||||
Series = new Series
|
||||
{
|
||||
Tags = new HashSet<int>()
|
||||
},
|
||||
Episodes = Builder<Episode>.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(e => e.AirDateUtc = DateTime.UtcNow)
|
||||
.Build()
|
||||
.ToList(),
|
||||
Release = new ReleaseInfo
|
||||
{
|
||||
PublishDate = DateTime.UtcNow.AddDays(-1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenSettings(bool airDateRestriction, int gracePeriod)
|
||||
{
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = airDateRestriction,
|
||||
AirDateGracePeriod = gracePeriod
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_profile_does_not_enforce_air_date_restriction()
|
||||
{
|
||||
GivenSettings(false, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_release_date_is_after_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
GivenSettings(true, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_release_date_with_grace_period_is_after_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
GivenSettings(true, -2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_release_date_is_the_same_as_air_date()
|
||||
{
|
||||
var airDate = DateTime.UtcNow;
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = airDate;
|
||||
_remoteEpisode.Release.PublishDate = airDate;
|
||||
|
||||
GivenSettings(true, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_air_date_is_null()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = null;
|
||||
|
||||
GivenSettings(true, -2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_is_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
GivenSettings(true, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_with_grace_period_is_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-3);
|
||||
|
||||
GivenSettings(true, -2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_is_after_air_date_and_grace_period_is_positive()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
GivenSettings(true, 2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_with_highest_grace_period_is_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = true,
|
||||
AirDateGracePeriod = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = true,
|
||||
AirDateGracePeriod = -5
|
||||
}
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_one_release_profile_does_not_allow_grabbing_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = true,
|
||||
AirDateGracePeriod = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = false,
|
||||
AirDateGracePeriod = 0
|
||||
}
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -331,6 +331,21 @@ public async Task scene_seasonsearch_should_use_seasonnumber_if_no_scene_number_
|
|||
criteria[0].SeasonNumber.Should().Be(7);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task scene_seasonsearch_should_skip_search_if_no_episodes_after_filtering()
|
||||
{
|
||||
WithEpisodes();
|
||||
_xemEpisodes.ForEach(e => e.EpisodeFileId = 1);
|
||||
|
||||
var allCriteria = WatchForSearchCriteria();
|
||||
|
||||
await Subject.SeasonSearch(_xemSeries.Id, 1, true, false, true, false);
|
||||
|
||||
var criteria = allCriteria.OfType<SeasonSearchCriteria>().ToList();
|
||||
|
||||
criteria.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task season_search_for_anime_should_search_for_each_monitored_episode()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
|
|
@ -25,7 +28,8 @@ public void Setup()
|
|||
{
|
||||
Id = 1,
|
||||
Title = "Title",
|
||||
Seasons = new List<Season>()
|
||||
Seasons = new List<Season>(),
|
||||
QualityProfile = new LazyLoaded<QualityProfile>(Builder<QualityProfile>.CreateNew().With(q => q.UpgradeAllowed = true).Build())
|
||||
};
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
|
|
@ -56,6 +60,23 @@ public void should_only_include_monitored_seasons()
|
|||
.Verify(v => v.SeasonSearch(_series.Id, It.IsAny<int>(), false, true, true, false), Times.Exactly(_series.Seasons.Count(s => s.Monitored)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_only_search_missing_if_profile_does_not_allow_upgrades()
|
||||
{
|
||||
_series.Seasons = new List<Season>
|
||||
{
|
||||
new Season { SeasonNumber = 0, Monitored = false },
|
||||
new Season { SeasonNumber = 1, Monitored = true }
|
||||
};
|
||||
|
||||
_series.QualityProfile.Value.UpgradeAllowed = false;
|
||||
|
||||
Subject.Execute(new SeriesSearchCommand { SeriesId = _series.Id, Trigger = CommandTrigger.Manual });
|
||||
|
||||
Mocker.GetMock<ISearchForReleases>()
|
||||
.Verify(v => v.SeasonSearch(_series.Id, It.IsAny<int>(), true, true, true, false), Times.Exactly(_series.Seasons.Count(s => s.Monitored)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_start_with_lower_seasons_first()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -168,6 +168,8 @@ public void should_parse_absolute_specials(string postTitle, string title, int a
|
|||
}
|
||||
|
||||
[TestCase("[Underwater] Another OVA - The Other -Karma- (BD 1080p) [3A561D0E].mkv", "Another", 0)]
|
||||
[TestCase("[sam] Long Series - NCOP [BD 1080p FLAC] [BBC3BC68].mkv", "Long Series", 0)]
|
||||
[TestCase("[sam] Long Series - NCED [BD 1080p FLAC] [BBC3BC68].mkv", "Long Series", 0)]
|
||||
public void should_parse_absolute_specials_without_absolute_number(string postTitle, string title, int absoluteEpisodeNumber)
|
||||
{
|
||||
var result = Parser.Parser.ParseTitle(postTitle);
|
||||
|
|
|
|||
|
|
@ -293,5 +293,55 @@ public void should_not_use_scene_season_number_from_xem_mapping_if_alias_matches
|
|||
|
||||
result.MappedSeasonNumber.Should().Be(sceneMapping.SceneSeasonNumber);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_tvdbid_matching_when_alias_without_year_is_found()
|
||||
{
|
||||
var alias = "Series Alias";
|
||||
|
||||
_parsedEpisodeInfo.SeriesTitle = $"{alias} {_series.Year}";
|
||||
_parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear = alias;
|
||||
_parsedEpisodeInfo.SeriesTitleInfo.Year = _series.Year;
|
||||
|
||||
Mocker.GetMock<ISceneMappingService>()
|
||||
.Setup(s => s.FindTvdbId(alias, It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Returns(_series.TvdbId);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(s => s.FindByTvdbId(_series.Id))
|
||||
.Returns(_series);
|
||||
|
||||
var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _series.ImdbId, null);
|
||||
|
||||
result.Series.Should().Be(_series);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_tvdbid_matching_when_alias_without_year_is_found_with_wrong_year()
|
||||
{
|
||||
var alias = "Series Alias";
|
||||
|
||||
_parsedEpisodeInfo.SeriesTitle = $"{alias} {_series.Year}";
|
||||
_parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear = alias;
|
||||
_parsedEpisodeInfo.SeriesTitleInfo.Year = _series.Year + 1;
|
||||
|
||||
Mocker.GetMock<ISceneMappingService>()
|
||||
.Setup(s => s.FindTvdbId(alias, It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Returns(_series.TvdbId);
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(s => s.FindByTvdbId(_series.Id))
|
||||
.Returns(_series);
|
||||
|
||||
var result = Subject.Map(_parsedEpisodeInfo, 0, 0, "", null);
|
||||
|
||||
result.Series.Should().BeNull();
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Verify(v => v.FindByTvdbId(It.IsAny<int>()), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration;
|
||||
|
||||
[Migration(226)]
|
||||
public class add_air_date_filtering_to_release_profiles : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("ReleaseProfiles").AddColumn("AirDateRestriction").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("ReleaseProfiles").AddColumn("AirDateGracePeriod").AsInt32().WithDefaultValue(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -75,5 +75,6 @@ public enum DownloadRejectionReason
|
|||
DiskCustomFormatScore,
|
||||
DiskCustomFormatScoreIncrement,
|
||||
DiskUpgradesNotAllowed,
|
||||
DiskNotUpgrade
|
||||
DiskNotUpgrade,
|
||||
BeforeAirDate
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class AirDateSpecification : IDownloadDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
private readonly ITermMatcherService _termMatcherService;
|
||||
|
||||
public AirDateSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_releaseProfileService = releaseProfileService;
|
||||
_termMatcherService = termMatcherService;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, ReleaseDecisionInformation information)
|
||||
{
|
||||
_logger.Debug("Checking if release meets air date restrictions: {0}", subject);
|
||||
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Series.Tags, subject.Release.IndexerId);
|
||||
|
||||
if (releaseProfiles.Empty())
|
||||
{
|
||||
_logger.Debug("No Release Profile, accepting");
|
||||
return DownloadSpecDecision.Accept();
|
||||
}
|
||||
|
||||
var bestProfile = releaseProfiles
|
||||
.OrderByDescending(p => p.AirDateRestriction ? 1 : 0)
|
||||
.ThenByDescending(p => p.AirDateGracePeriod)
|
||||
.First();
|
||||
|
||||
if (!bestProfile.AirDateRestriction)
|
||||
{
|
||||
_logger.Debug("Release Profile does not prevent grabbing before release date, accepting");
|
||||
return DownloadSpecDecision.Accept();
|
||||
}
|
||||
|
||||
var releaseDate = subject.Release.PublishDate;
|
||||
var gracePeriod = bestProfile.AirDateGracePeriod;
|
||||
|
||||
foreach (var episode in subject.Episodes)
|
||||
{
|
||||
var airDate = episode.AirDateUtc;
|
||||
|
||||
if (!airDate.HasValue)
|
||||
{
|
||||
_logger.Debug("No air date available, rejecting");
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.BeforeAirDate, "No air date available");
|
||||
}
|
||||
|
||||
var adjustedAirDate = airDate.Value.AddDays(gracePeriod);
|
||||
|
||||
if (releaseDate < adjustedAirDate)
|
||||
{
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.BeforeAirDate, "Release date {0} is before adjusted air date of {1} (Air Date: {2}. Grace period {3} days)", releaseDate, adjustedAirDate, airDate, gracePeriod);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("All episodes within air date limitations, allowing");
|
||||
return DownloadSpecDecision.Accept();
|
||||
}
|
||||
|
||||
private ReleaseProfile FindBestProfile(List<ReleaseProfile> releaseProfiles)
|
||||
{
|
||||
return releaseProfiles
|
||||
.OrderBy(p => p.AirDateRestriction ? 0 : 1)
|
||||
.ThenBy(p => p.AirDateGracePeriod)
|
||||
.ThenBy(p => p.AirDateRestriction ? 0 : 1)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,12 @@ public async Task<List<DownloadDecision>> SeasonSearch(int seriesId, int seasonN
|
|||
episodes = episodes.Where(e => !e.HasFile).ToList();
|
||||
}
|
||||
|
||||
if (episodes.Count == 0)
|
||||
{
|
||||
_logger.Debug("No wanted episodes found for season {0}", seasonNumber);
|
||||
return new List<DownloadDecision>();
|
||||
}
|
||||
|
||||
return await SeasonSearch(seriesId, seasonNumber, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public void Execute(SeriesSearchCommand message)
|
|||
var series = _seriesService.GetSeries(message.SeriesId);
|
||||
var downloadedCount = 0;
|
||||
var userInvokedSearch = message.Trigger == CommandTrigger.Manual;
|
||||
var profile = series.QualityProfile.Value;
|
||||
|
||||
if (series.Seasons.None(s => s.Monitored))
|
||||
{
|
||||
|
|
@ -64,7 +65,7 @@ public void Execute(SeriesSearchCommand message)
|
|||
continue;
|
||||
}
|
||||
|
||||
var decisions = _releaseSearchService.SeasonSearch(message.SeriesId, season.SeasonNumber, false, true, userInvokedSearch, false).GetAwaiter().GetResult();
|
||||
var decisions = _releaseSearchService.SeasonSearch(message.SeriesId, season.SeasonNumber, !profile.UpgradeAllowed, true, userInvokedSearch, false).GetAwaiter().GetResult();
|
||||
var processDecisions = _processDownloadDecisions.ProcessDecisions(decisions).GetAwaiter().GetResult();
|
||||
downloadedCount += processDecisions.Grabbed.Count;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@
|
|||
"AgeWhenGrabbed": "Age (when grabbed)",
|
||||
"Agenda": "Agenda",
|
||||
"AirDate": "Air Date",
|
||||
"AirDateGracePeriod": "Air Date Grace Period",
|
||||
"AirDateGracePeriodHelpText": "Negative values allow grabbing before the air date, positive values prevent grabbing after the air date.",
|
||||
"AirDateRestriction": "Reject Unaired Releases",
|
||||
"AirDateRestrictionHelpText": "Prevents {appName} from grabbing releases that contain episodes that have not yet aired.",
|
||||
"Airs": "Airs",
|
||||
"AirsDateAtTimeOn": "{date} at {time} on {networkLabel}",
|
||||
"AirsTbaOn": "TBA on {networkLabel}",
|
||||
|
|
@ -786,6 +790,7 @@
|
|||
"Formats": "Formats",
|
||||
"Forums": "Forums",
|
||||
"FreeSpace": "Free Space",
|
||||
"Friday": "Friday",
|
||||
"From": "From",
|
||||
"FullColorEvents": "Full Color Events",
|
||||
"FullColorEventsHelpText": "Altered style to color the entire event with the status color, instead of just the left edge. Does not apply to Agenda",
|
||||
|
|
@ -1863,6 +1868,7 @@
|
|||
"RssSyncIntervalHelpText": "Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)",
|
||||
"RssSyncIntervalHelpTextWarning": "This will apply to all indexers, please follow the rules set forth by them",
|
||||
"Runtime": "Runtime",
|
||||
"Saturday": "Saturday",
|
||||
"Save": "Save",
|
||||
"SaveChanges": "Save Changes",
|
||||
"SaveSettings": "Save Settings",
|
||||
|
|
@ -2090,6 +2096,7 @@
|
|||
"TheTvdb": "TheTVDB",
|
||||
"Theme": "Theme",
|
||||
"ThemeHelpText": "Change Application UI Theme, 'Auto' Theme will use your OS Theme to set Light or Dark mode. Inspired by Theme.Park",
|
||||
"Thursday": "Thursday",
|
||||
"Threshold": "Threshold",
|
||||
"Time": "Time",
|
||||
"TimeFormat": "Time Format",
|
||||
|
|
@ -2121,6 +2128,7 @@
|
|||
"TotalFileSize": "Total File Size",
|
||||
"TotalRecords": "Total records: {totalRecords}",
|
||||
"TotalSpace": "Total Space",
|
||||
"Tuesday": "Tuesday",
|
||||
"Trace": "Trace",
|
||||
"True": "True",
|
||||
"TvdbId": "TVDB ID",
|
||||
|
|
@ -2222,6 +2230,7 @@
|
|||
"Wanted": "Wanted",
|
||||
"Warn": "Warn",
|
||||
"Warning": "Warning",
|
||||
"Wednesday": "Wednesday",
|
||||
"Week": "Week",
|
||||
"WeekColumnHeader": "Week Column Header",
|
||||
"WeekColumnHeaderHelpText": "Shown above each column when week is the active view",
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@
|
|||
"CustomFormatsSpecificationReleaseGroup": "Groupe de versions",
|
||||
"CustomFormatsSpecificationResolution": "Résolution",
|
||||
"CustomFormatsSpecificationSource": "Source",
|
||||
"Cutoff": "Seuil",
|
||||
"Cutoff": "Limite",
|
||||
"CutoffNotMet": "Seuil non atteint",
|
||||
"CutoffUnmet": "Seuil non atteint",
|
||||
"CutoffUnmetLoadError": "Erreur lors du chargement des éléments dont le seuil n'est pas atteint",
|
||||
|
|
@ -843,6 +843,8 @@
|
|||
"Ignored": "Ignoré",
|
||||
"IgnoredAddresses": "Adresses ignorées",
|
||||
"ImageBanner": "bannière",
|
||||
"ImageFanart": "fanart",
|
||||
"ImagePoster": "affiche",
|
||||
"ImageSeason": "saison",
|
||||
"Images": "Images",
|
||||
"ImdbId": "IMDb ID",
|
||||
|
|
@ -991,6 +993,7 @@
|
|||
"IncludeCustomFormatWhenRenaming": "Inclure un format personnalisé lors du changement de nom",
|
||||
"IncludeCustomFormatWhenRenamingHelpText": "Inclure dans le format de renommage {Formats personnalisés}",
|
||||
"IncludeHealthWarnings": "Inclure les avertissements de santé",
|
||||
"IncludeSpecials": "Inclure les offres spéciales",
|
||||
"IncludeUnmonitored": "Inclure les non surveillés",
|
||||
"Indexer": "Indexeur",
|
||||
"IndexerDownloadClientHealthCheckMessage": "Indexeurs avec des clients de téléchargement invalides : {indexerNames}.",
|
||||
|
|
@ -1092,6 +1095,7 @@
|
|||
"InstanceNameHelpText": "Nom de l'instance dans l'onglet et pour le nom de l'application Syslog",
|
||||
"InteractiveImport": "Importation interactive",
|
||||
"InteractiveImportLoadError": "Impossible de charger les éléments d'importation manuelle",
|
||||
"InteractiveImportMultipleQueueItems": "Éléments de file d'attente multiples",
|
||||
"InteractiveImportNoEpisode": "Un ou plusieurs épisodes doivent être choisis pour chaque fichier sélectionné",
|
||||
"InteractiveImportNoFilesFound": "Aucun fichier vidéo n'a été trouvé dans le dossier sélectionné",
|
||||
"InteractiveImportNoImportMode": "Un mode d'importation doit être sélectionné",
|
||||
|
|
@ -1194,9 +1198,11 @@
|
|||
"MaximumSizeHelpText": "Taille maximale en Mo pour qu'une version soit récupérée. Réglez sur zéro pour une taille illimitée",
|
||||
"Mechanism": "Mécanisme",
|
||||
"MediaInfo": "Informations médias",
|
||||
"MediaInfoAudioStreamHeader": "Flux audio #{number}",
|
||||
"MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages supporte un suffixe `:EN+DE` vous permettant de filtrer les langues incluses dans le nom de fichier. Utilisez `-DE` pour exclure des langues spécifiques. En ajoutant `+` (par exemple `:EN+`), vous obtiendrez `[EN]`/`[EN+--]`/`[--]` en fonction des langues exclues. Par exemple `{MediaInfo Full:EN+DE}`.",
|
||||
"MediaInfoFootNote2": "MediaInfo AudioLanguages exclue l’anglais s’il s’agit de la seule langue. Utiliser MediaInfo AudioLanguagesAll pour inclure ceux seulement en anglais",
|
||||
"MediaInfoForced": "Forcé",
|
||||
"MediaInfoHearingImpaired": "Malentendant",
|
||||
"MediaInfoSubtitlesHeader": "Sous-titres",
|
||||
"MediaManagement": "Gestion des médias",
|
||||
"MediaManagementSettings": "Paramètres de gestion des médias",
|
||||
|
|
@ -2076,6 +2082,7 @@
|
|||
"TheTvdb": "TheTVDB",
|
||||
"Theme": "Thème",
|
||||
"ThemeHelpText": "Modifiez le thème de l'interface utilisateur de l'application, le thème « Auto » utilisera le thème de votre système d'exploitation pour définir le mode clair ou sombre. Inspiré par Theme.Park",
|
||||
"Threshold": "Seuil",
|
||||
"Time": "Heure",
|
||||
"TimeFormat": "Format de l'heure",
|
||||
"TimeLeft": "Temps restant",
|
||||
|
|
@ -2134,6 +2141,7 @@
|
|||
"Unknown": "Inconnu",
|
||||
"UnknownDownloadState": "État de téléchargement inconnu : {state}",
|
||||
"UnknownEventTooltip": "Événement inconnu",
|
||||
"UnknownSeriesItems": "Éléments de la série inconnus",
|
||||
"Unlimited": "Illimité",
|
||||
"UnmappedFilesOnly": "Fichiers non mappés uniquement",
|
||||
"UnmappedFolders": "Dossiers non mappés",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"AddConditionError": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen",
|
||||
"AddConditionImplementation": "Legg til betingelse - {implementationName}",
|
||||
"AddConnection": "Legg til tilkobling",
|
||||
"AddConnectionImplementation": "Legg til tilkobling - {implementationName}",
|
||||
"AddConnectionImplementation": "Legg til betingelse - {implementationName}",
|
||||
"AddCustomFilter": "Legg til eget filter",
|
||||
"AddCustomFormat": "Nytt Egendefinert format",
|
||||
"AddCustomFormatError": "Kunne ikke legge til nytt egendefinert format, vennligst prøv på nytt.",
|
||||
|
|
@ -52,8 +52,15 @@
|
|||
"Age": "Alder",
|
||||
"Agenda": "Agenda",
|
||||
"AllTitles": "Alle titler",
|
||||
"AnalyseVideoFilesHelpText": "Trekke ut informasjon som oppløsning, kjøretid og kodek informasjon fra filer. Dette forutsetter att {appName}leser deler av filen. dette kan forutsake høy disk eller nettverks aktivitet når filer skannes.",
|
||||
"ApiKeyValidationHealthCheckMessage": "Vennligst oppdater din API-nøkkel til å være minst {length} tegn lang. Du kan gjøre dette via innstillinger eller konfigurasjonsfilen",
|
||||
"AppDataDirectory": "AppData -katalog",
|
||||
"AppUpdated": "{appName} Oppdatert",
|
||||
"ApplicationUrlHelpText": "Denne applikasjonens eksterne URL inkludert http(s)://, port og URL base",
|
||||
"ApplyChanges": "Bekreft endringer",
|
||||
"AudioLanguages": "Flerspråklig",
|
||||
"AuthenticationMethodHelpTextWarning": "Vennligst velg en valid autentiserings metode.",
|
||||
"AuthenticationRequired": "Verefisering påkrevd",
|
||||
"AutomaticAdd": "Legg til automatisk",
|
||||
"CalendarOptions": "Kalenderinnstillinger",
|
||||
"ClearBlocklistMessageText": "Er du sikker på at du vil fjerne alle elementer fra blokkeringslisten?",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
|
|
@ -176,20 +177,26 @@ public void Cleanup()
|
|||
|
||||
_logger.Info("Removing items older than {0} days from the recycling bin", cleanupDays);
|
||||
|
||||
var removedFiles = new List<string>();
|
||||
var skippedFiles = new List<string>();
|
||||
|
||||
foreach (var file in _diskProvider.GetFiles(_configService.RecycleBin, true))
|
||||
{
|
||||
if (_diskProvider.FileGetLastWrite(file).AddDays(cleanupDays) > DateTime.UtcNow)
|
||||
{
|
||||
_logger.Debug("File hasn't expired yet, skipping: {0}", file);
|
||||
skippedFiles.Add(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
removedFiles.Add(file);
|
||||
_logger.Debug("File expired, deleting: {0}", file);
|
||||
_diskProvider.DeleteFile(file);
|
||||
}
|
||||
|
||||
_diskProvider.RemoveEmptySubfolders(_configService.RecycleBin);
|
||||
|
||||
_logger.Debug("Recycling Bin has been cleaned up.");
|
||||
_logger.Debug("Recycling Bin has been cleaned up. Removed: {0}. Skipped: {1}", removedFiles.Count, skippedFiles.Count);
|
||||
}
|
||||
|
||||
private void SetLastWriteTime(string file, DateTime dateTime)
|
||||
|
|
@ -197,13 +204,16 @@ private void SetLastWriteTime(string file, DateTime dateTime)
|
|||
// Swallow any IOException that may be thrown due to "Invalid parameter"
|
||||
try
|
||||
{
|
||||
_logger.Trace("Setting last write time for file: {0}", file);
|
||||
_diskProvider.FileSetLastWriteTime(file, dateTime);
|
||||
}
|
||||
catch (IOException)
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to set last write time for file: {0}", file);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to set last write time for file: {0}", file);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -141,8 +141,9 @@ private void UpdateSectionPath(string seriesRelativePath, PlexSection section, P
|
|||
var separator = location.Path.Contains('\\') ? "\\" : "/";
|
||||
var locationRelativePath = seriesRelativePath.Replace("\\", separator).Replace("/", separator);
|
||||
|
||||
// Plex location paths trim trailing extraneous separator characters, so it doesn't need to be trimmed
|
||||
var pathToUpdate = $"{location.Path}{separator}{locationRelativePath}";
|
||||
// Plex location paths trim trailing extraneous separator characters,
|
||||
// unless it's a Windows drive letter (S:\) that needs to be trimmed.
|
||||
var pathToUpdate = $"{location.Path.TrimEnd(separator)}{separator}{locationRelativePath}";
|
||||
|
||||
_logger.Debug("Updating section location, {0}", location.Path);
|
||||
_plexServerProxy.Update(section.Id, pathToUpdate, settings);
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ public static class Parser
|
|||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
// Anime OVA special
|
||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[-_. ]+(?<special>special|ova|ovd)).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
|
||||
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:[-_. ]+(?<special>special|ova|ovd|ncop|nced)).*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,25 @@ private Series GetSeriesByAllTitles(ParsedEpisodeInfo parsedEpisodeInfo)
|
|||
return foundSeries;
|
||||
}
|
||||
|
||||
private Series GetSeriesAliasTitleAndYear(ParsedEpisodeInfo parsedEpisodeInfo)
|
||||
{
|
||||
var year = parsedEpisodeInfo.SeriesTitleInfo.Year;
|
||||
var titleWithoutyear = parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear;
|
||||
var tvdbId = _sceneMappingService.FindTvdbId(titleWithoutyear, parsedEpisodeInfo.ReleaseTitle, parsedEpisodeInfo.SeasonNumber);
|
||||
|
||||
if (tvdbId.HasValue)
|
||||
{
|
||||
var series = _seriesService.FindByTvdbId(tvdbId.Value);
|
||||
|
||||
if (series.Year == year)
|
||||
{
|
||||
return series;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, string imdbId, SearchCriteriaBase searchCriteria = null)
|
||||
{
|
||||
return Map(parsedEpisodeInfo, tvdbId, tvRageId, imdbId, null, searchCriteria);
|
||||
|
|
@ -449,6 +468,12 @@ private FindSeriesResult FindSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvd
|
|||
{
|
||||
series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, parsedEpisodeInfo.SeriesTitleInfo.Year);
|
||||
matchType = SeriesMatchType.Title;
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
series = GetSeriesAliasTitleAndYear(parsedEpisodeInfo);
|
||||
matchType = SeriesMatchType.Alias;
|
||||
}
|
||||
}
|
||||
|
||||
if (series == null && tvdbId > 0)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ public class ReleaseProfile : ModelBase
|
|||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
public bool AirDateRestriction { get; set; }
|
||||
public int AirDateGracePeriod { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public HashSet<int> ExcludedTags { get; set; }
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@
|
|||
<PackageReference Include="Diacritical.Net" Version="1.0.5" />
|
||||
<PackageReference Include="Equ" Version="2.3.0" />
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
|
||||
<PackageReference Include="MiniProfiler.AspNetCore" Version="4.5.4" />
|
||||
<PackageReference Include="Openur.FFMpegCore" Version="5.4.0.31" />
|
||||
<PackageReference Include="Openur.FFprobeStatic" Version="8.0.1.302" />
|
||||
<PackageReference Include="Polly" Version="8.6.5" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Core" Version="8.0.1" />
|
||||
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="8.0.1" />
|
||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" />
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.3" />
|
||||
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
|
||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" />
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Resources.Extensions" Version="10.0.2" />
|
||||
<PackageReference Include="System.Resources.Extensions" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" />
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public ReleaseProfileController(IReleaseProfileService releaseProfileService, II
|
|||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty())
|
||||
if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty() && !restriction.AirDateRestriction)
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ public class ReleaseProfileResource : RestResource
|
|||
// Is List<string>, string or JArray, we accept 'string' with POST for backward compatibility
|
||||
public object Required { get; set; }
|
||||
public object Ignored { get; set; }
|
||||
public bool AirDateRestriction { get; set; }
|
||||
public int AirDateGracePeriod { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public HashSet<int> ExcludedTags { get; set; }
|
||||
|
|
@ -42,6 +44,8 @@ public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
|||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? new List<string>(),
|
||||
Ignored = model.Ignored ?? new List<string>(),
|
||||
AirDateRestriction = model.AirDateRestriction,
|
||||
AirDateGracePeriod = model.AirDateGracePeriod,
|
||||
IndexerId = model.IndexerIds.FirstOrDefault(0),
|
||||
Tags = new HashSet<int>(model.Tags),
|
||||
ExcludedTags = new HashSet<int>(model.ExcludedTags)
|
||||
|
|
@ -62,6 +66,8 @@ public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
|||
Enabled = resource.Enabled,
|
||||
Required = resource.MapRequired(),
|
||||
Ignored = resource.MapIgnored(),
|
||||
AirDateRestriction = resource.AirDateRestriction,
|
||||
AirDateGracePeriod = resource.AirDateGracePeriod,
|
||||
IndexerIds = new List<int> { resource.IndexerId },
|
||||
Tags = new HashSet<int>(resource.Tags),
|
||||
ExcludedTags = new HashSet<int>(resource.ExcludedTags)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageReference Include="NLog" Version="5.5.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" />
|
||||
|
|
|
|||
35
src/Sonarr.Api.V5/Localization/LanguageController.cs
Normal file
35
src/Sonarr.Api.V5/Localization/LanguageController.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Languages;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Localization;
|
||||
|
||||
[V5ApiController]
|
||||
public class LanguageController : RestController<LanguageResource>
|
||||
{
|
||||
protected override LanguageResource GetResourceById(int id)
|
||||
{
|
||||
var language = (Language)id;
|
||||
|
||||
return new LanguageResource
|
||||
{
|
||||
Id = (int)language,
|
||||
Name = language.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<LanguageResource> GetAll()
|
||||
{
|
||||
var languageResources = Language.All.Select(l => new LanguageResource
|
||||
{
|
||||
Id = (int)l,
|
||||
Name = l.ToString()
|
||||
})
|
||||
.OrderBy(l => l.Id > 0).ThenBy(l => l.Name)
|
||||
.ToList();
|
||||
|
||||
return languageResources;
|
||||
}
|
||||
}
|
||||
12
src/Sonarr.Api.V5/Localization/LanguageResource.cs
Normal file
12
src/Sonarr.Api.V5/Localization/LanguageResource.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Localization;
|
||||
|
||||
public class LanguageResource : RestResource
|
||||
{
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public new int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? NameLower => Name?.ToLowerInvariant();
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ public ReleaseProfileController(IReleaseProfileService releaseProfileService, II
|
|||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.Required.Empty() && restriction.Ignored.Empty())
|
||||
if (restriction.Required.Empty() && restriction.Ignored.Empty() && !restriction.AirDateRestriction)
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ public class ReleaseProfileResource : RestResource
|
|||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; } = [];
|
||||
public List<string> Ignored { get; set; } = [];
|
||||
public bool AirDateRestriction { get; set; }
|
||||
public int AirDateGracePeriod { get; set; }
|
||||
public List<int> IndexerIds { get; set; } = [];
|
||||
public HashSet<int> Tags { get; set; } = [];
|
||||
public HashSet<int> ExcludedTags { get; set; } = [];
|
||||
|
|
@ -25,6 +27,8 @@ public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
|||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? [],
|
||||
Ignored = model.Ignored ?? [],
|
||||
AirDateRestriction = model.AirDateRestriction,
|
||||
AirDateGracePeriod = model.AirDateGracePeriod,
|
||||
IndexerIds = model.IndexerIds ?? [],
|
||||
Tags = model.Tags ?? [],
|
||||
ExcludedTags = model.ExcludedTags ?? [],
|
||||
|
|
@ -40,6 +44,8 @@ public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
|||
Enabled = resource.Enabled,
|
||||
Required = resource.Required,
|
||||
Ignored = resource.Ignored,
|
||||
AirDateRestriction = resource.AirDateRestriction,
|
||||
AirDateGracePeriod = resource.AirDateGracePeriod,
|
||||
IndexerIds = resource.IndexerIds,
|
||||
Tags = resource.Tags,
|
||||
ExcludedTags = resource.ExcludedTags
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageReference Include="NLog" Version="5.5.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" />
|
||||
|
|
|
|||
|
|
@ -2222,6 +2222,351 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/settings/mediamanagement": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"MediaManagementSettings"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaManagementSettingsResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/settings/mediamanagement/{id}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"MediaManagementSettings"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaManagementSettingsResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaManagementSettingsResource"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaManagementSettingsResource"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaManagementSettingsResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"MediaManagementSettings"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MediaManagementSettingsResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/metadata": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceSave",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/metadata/{id}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceSave",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/metadata/schema": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/metadata/test": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/metadata/testall": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/metadata/action/{name}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v5/wanted/missing": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -6157,6 +6502,14 @@
|
|||
],
|
||||
"type": "string"
|
||||
},
|
||||
"EpisodeTitleRequiredType": {
|
||||
"enum": [
|
||||
"always",
|
||||
"bulkSeasonReleases",
|
||||
"never"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"EpisodesMonitoredResource": {
|
||||
"required": [
|
||||
"episodeIds"
|
||||
|
|
@ -6250,6 +6603,14 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FileDateType": {
|
||||
"enum": [
|
||||
"none",
|
||||
"localAirDate",
|
||||
"utcAirDate"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HealthCheckReason": {
|
||||
"enum": [
|
||||
"appDataLocation",
|
||||
|
|
@ -7000,6 +7361,153 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MediaManagementSettingsResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"autoUnmonitorPreviouslyDownloadedEpisodes": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"recycleBin": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"recycleBinCleanupDays": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"downloadPropersAndRepacks": {
|
||||
"$ref": "#/components/schemas/ProperDownloadTypes"
|
||||
},
|
||||
"createEmptySeriesFolders": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deleteEmptyFolders": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fileDate": {
|
||||
"$ref": "#/components/schemas/FileDateType"
|
||||
},
|
||||
"rescanAfterRefresh": {
|
||||
"$ref": "#/components/schemas/RescanAfterRefreshType"
|
||||
},
|
||||
"setPermissionsLinux": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"chmodFolder": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"chownGroup": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"episodeTitleRequired": {
|
||||
"$ref": "#/components/schemas/EpisodeTitleRequiredType"
|
||||
},
|
||||
"skipFreeSpaceCheckWhenImporting": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"minimumFreeSpaceWhenImporting": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"copyUsingHardlinks": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"useScriptImport": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"scriptImportPath": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"importExtraFiles": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"extraFileExtensions": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"enableMediaInfo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"userRejectedExtensions": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"seasonPackUpgrade": {
|
||||
"$ref": "#/components/schemas/SeasonPackUpgradeType"
|
||||
},
|
||||
"seasonPackUpgradeThreshold": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MetadataResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Field"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"implementationName": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"implementation": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"configContract": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"infoLink": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"message": {
|
||||
"$ref": "#/components/schemas/ProviderMessage"
|
||||
},
|
||||
"tags": {
|
||||
"uniqueItems": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"presets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataResource"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"enable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"MissingSubresource": {
|
||||
"enum": [
|
||||
"series",
|
||||
|
|
@ -7394,6 +7902,14 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ProperDownloadTypes": {
|
||||
"enum": [
|
||||
"preferAndUpgrade",
|
||||
"doNotUpgrade",
|
||||
"doNotPrefer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ProviderMessage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -8337,6 +8853,14 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"RescanAfterRefreshType": {
|
||||
"enum": [
|
||||
"always",
|
||||
"afterManual",
|
||||
"never"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Revision": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -8420,6 +8944,14 @@
|
|||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SeasonPackUpgradeType": {
|
||||
"enum": [
|
||||
"all",
|
||||
"threshold",
|
||||
"any"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SeasonPassResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -9493,6 +10025,12 @@
|
|||
{
|
||||
"name": "ManualImport"
|
||||
},
|
||||
{
|
||||
"name": "MediaManagementSettings"
|
||||
},
|
||||
{
|
||||
"name": "Metadata"
|
||||
},
|
||||
{
|
||||
"name": "Missing"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue