Merge remote-tracking branch 'origin/development' into development

pull/2644/head
morpheus65535 3 months ago
commit 8e8311186d

@ -96,6 +96,7 @@ validators = [
Validator('general.path_mappings_movie', must_exist=True, default=[], is_type_of=list),
Validator('general.serie_tag_enabled', must_exist=True, default=False, is_type_of=bool),
Validator('general.movie_tag_enabled', must_exist=True, default=False, is_type_of=bool),
Validator('general.remove_profile_tags', must_exist=True, default=[], is_type_of=list, condition=validate_tags),
Validator('general.serie_default_enabled', must_exist=True, default=False, is_type_of=bool),
Validator('general.serie_default_profile', must_exist=True, default='', is_type_of=(int, str)),
Validator('general.movie_default_enabled', must_exist=True, default=False, is_type_of=bool),
@ -468,6 +469,7 @@ array_keys = ['excluded_tags',
'enabled_integrations',
'path_mappings',
'path_mappings_movie',
'remove_profile_tags',
'language_equals',
'blacklisted_languages',
'blacklisted_providers']

@ -152,6 +152,10 @@ def movieParser(movie, action, tags_dict, language_profiles, movie_default_profi
tag_profile = get_matching_profile(tags, language_profiles)
if tag_profile:
parsed_movie['profileId'] = tag_profile
remove_profile_tags_list = settings.general.remove_profile_tags
if len(remove_profile_tags_list) > 0:
if set(tags) & set(remove_profile_tags_list):
parsed_movie['profileId'] = None
return parsed_movie

@ -78,6 +78,10 @@ def seriesParser(show, action, tags_dict, language_profiles, serie_default_profi
tag_profile = get_matching_profile(tags, language_profiles)
if tag_profile:
parsed_series['profileId'] = tag_profile
remove_profile_tags_list = settings.general.remove_profile_tags
if len(remove_profile_tags_list) > 0:
if set(tags) & set(remove_profile_tags_list):
parsed_series['profileId'] = None
return parsed_series

@ -15,12 +15,11 @@
"@typescript-eslint/no-unused-vars": "warn"
},
"extends": [
"react-app",
"plugin:react-hooks/recommended",
"eslint:recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["testing-library", "simple-import-sort"],
"plugins": ["testing-library", "simple-import-sort", "react-refresh"],
"overrides": [
{
"files": [
@ -63,6 +62,7 @@
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest"

File diff suppressed because it is too large Load Diff

@ -44,14 +44,16 @@
"@types/node": "^20.12.6",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2",
"clsx": "^2.1.0",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-simple-import-sort": "^12.1.0",
"eslint-plugin-testing-library": "^6.2.0",
"husky": "^9.0.11",

@ -25,23 +25,6 @@ const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
});
};
export function useEpisodesByIds(ids: number[]) {
const client = useQueryClient();
const query = useQuery({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, ids],
queryFn: () => api.episodes.byEpisodeId(ids),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheEpisodes(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useEpisodesBySeriesId(id: number) {
const client = useQueryClient();
@ -87,10 +70,11 @@ export function useEpisodeAddBlacklist() {
},
onSuccess: (_, { seriesId }) => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Series, seriesId],
});
},
@ -105,8 +89,8 @@ export function useEpisodeDeleteBlacklist() {
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
api.episodes.deleteBlacklist(param.all, param.form),
onSuccess: (_) => {
client.invalidateQueries({
onSuccess: () => {
void client.invalidateQueries({
queryKey: [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
});
},

@ -15,23 +15,6 @@ const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
});
};
export function useMoviesByIds(ids: number[]) {
const client = useQueryClient();
const query = useQuery({
queryKey: [QueryKeys.Movies, ...ids],
queryFn: () => api.movies.movies(ids),
});
useEffect(() => {
if (query.isSuccess && query.data) {
cacheMovies(client, query.data);
}
}, [query.isSuccess, query.data, client]);
return query;
}
export function useMovieById(id: number) {
return useQuery({
queryKey: [QueryKeys.Movies, id],
@ -74,12 +57,13 @@ export function useMovieModification() {
onSuccess: (_, form) => {
form.id.forEach((v) => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Movies, v],
});
});
// TODO: query less
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
},
@ -93,7 +77,7 @@ export function useMovieAction() {
mutationFn: (form: FormType.MoviesAction) => api.movies.action(form),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
},
@ -125,10 +109,11 @@ export function useMovieAddBlacklist() {
},
onSuccess: (_, { id }) => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Movies, id],
});
},
@ -143,8 +128,8 @@ export function useMovieDeleteBlacklist() {
mutationFn: (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
api.movies.deleteBlacklist(param.all, param.form),
onSuccess: (_, param) => {
client.invalidateQueries({
onSuccess: () => {
void client.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
},

@ -54,22 +54,27 @@ export function useSettingsMutation() {
mutationFn: (data: LooseObject) => api.system.updateSettings(data),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Series],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Episodes],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Movies],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Wanted],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.Badges],
});
},
@ -101,7 +106,7 @@ export function useDeleteLogs() {
mutationFn: () => api.system.deleteLogs(),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Logs],
});
},
@ -128,11 +133,12 @@ export function useSystemAnnouncementsAddDismiss() {
return api.system.addAnnouncementsDismiss(hash);
},
onSuccess: (_, { hash }) => {
client.invalidateQueries({
onSuccess: () => {
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Announcements],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Badges],
});
},
@ -156,10 +162,11 @@ export function useRunTask() {
mutationFn: (id: string) => api.system.runTask(id),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
});
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
@ -180,7 +187,7 @@ export function useCreateBackups() {
mutationFn: () => api.system.createBackups(),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
@ -194,7 +201,7 @@ export function useRestoreBackups() {
mutationFn: (filename: string) => api.system.restoreBackups(filename),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},
@ -208,7 +215,7 @@ export function useDeleteBackups() {
mutationFn: (filename: string) => api.system.deleteBackups(filename),
onSuccess: () => {
client.invalidateQueries({
void client.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Backups],
});
},

@ -16,7 +16,6 @@ type MutateActionProps<DATA, VAR> = Omit<
function MutateAction<DATA, VAR>({
mutation,
noReset,
onSuccess,
onError,
args,

@ -15,7 +15,6 @@ type MutateButtonProps<DATA, VAR> = Omit<
function MutateButton<DATA, VAR>({
mutation,
noReset,
onSuccess,
onError,
args,

@ -12,7 +12,7 @@ interface QueryOverlayProps {
const QueryOverlay: FunctionComponent<QueryOverlayProps> = ({
children,
global = false,
result: { isLoading, isError, error },
result: { isLoading },
}) => {
return (
<LoadingProvider value={isLoading}>

@ -7,7 +7,7 @@ import {
Select,
SelectProps,
} from "@mantine/core";
import { isNull, isUndefined, noop } from "lodash";
import { isNull, isUndefined } from "lodash";
import { LOG } from "@/utilities/console";
export type SelectorOption<T> = Override<
@ -49,10 +49,7 @@ export type GroupedSelectorProps<T> = Override<
>;
export function GroupedSelector<T>({
value,
options,
getkey = DefaultKeyBuilder,
onOptionSubmit = noop,
...select
}: GroupedSelectorProps<T>) {
return (

@ -5,11 +5,8 @@ import { ModalSettings } from "@mantine/modals/lib/context";
import { ModalComponent, ModalIdContext } from "./WithModal";
export function useModals() {
const {
openContextModal: openMantineContextModal,
closeContextModal: closeContextModalRaw,
...rest
} = useMantineModals();
const { openContextModal: openMantineContextModal, ...rest } =
useMantineModals();
const openContextModal = useCallback(
<ARGS extends {}>(
@ -26,7 +23,7 @@ export function useModals() {
[openMantineContextModal],
);
const closeContextModal = useCallback(
const closeContext = useCallback(
(modal: ModalComponent) => {
rest.closeModal(modal.modalKey);
},
@ -43,7 +40,7 @@ export function useModals() {
// TODO: Performance
return useMemo(
() => ({ openContextModal, closeContextModal, closeSelf, ...rest }),
[closeContextModal, closeSelf, openContextModal, rest],
() => ({ openContextModal, closeContext, closeSelf, ...rest }),
[closeContext, closeSelf, openContextModal, rest],
);
}

@ -40,13 +40,17 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (ids) => {
LOG("info", "Invalidating series", ids);
ids.forEach((id) => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.Series, id] });
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, id],
});
});
},
delete: (ids) => {
LOG("info", "Invalidating series", ids);
ids.forEach((id) => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.Series, id] });
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, id],
});
});
},
},
@ -55,13 +59,17 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
update: (ids) => {
LOG("info", "Invalidating movies", ids);
ids.forEach((id) => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.Movies, id] });
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, id],
});
});
},
delete: (ids) => {
LOG("info", "Invalidating movies", ids);
ids.forEach((id) => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.Movies, id] });
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, id],
});
});
},
},
@ -78,7 +86,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
id,
]);
if (episode !== undefined) {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, episode.sonarrSeriesId],
});
}
@ -92,7 +100,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
id,
]);
if (episode !== undefined) {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Series, episode.sonarrSeriesId],
});
}
@ -101,28 +109,28 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
},
{
key: "episode-wanted",
update: (ids) => {
update: () => {
// Find a better way to update wanted
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
delete: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
},
{
key: "movie-wanted",
update: (ids) => {
update: () => {
// Find a better way to update wanted
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
delete: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
@ -130,13 +138,13 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "settings",
any: () => {
queryClient.invalidateQueries({ queryKey: [QueryKeys.System] });
void queryClient.invalidateQueries({ queryKey: [QueryKeys.System] });
},
},
{
key: "languages",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Languages],
});
},
@ -144,7 +152,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "badges",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Badges],
});
},
@ -152,7 +160,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "movie-history",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.History],
});
},
@ -160,7 +168,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "movie-blacklist",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Blacklist],
});
},
@ -168,7 +176,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "episode-history",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.History],
});
},
@ -176,7 +184,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "episode-blacklist",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Blacklist],
});
},
@ -184,7 +192,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "reset-episode-wanted",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Episodes, QueryKeys.Wanted],
});
},
@ -192,7 +200,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "reset-movie-wanted",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.Movies, QueryKeys.Wanted],
});
},
@ -200,7 +208,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] {
{
key: "task",
any: () => {
queryClient.invalidateQueries({
void queryClient.invalidateQueries({
queryKey: [QueryKeys.System, QueryKeys.Tasks],
});
},

@ -1,7 +1,9 @@
import { FunctionComponent } from "react";
import { Text as MantineText } from "@mantine/core";
import { useLanguageProfiles, useLanguages } from "@/apis/hooks";
import {
Check,
Chips,
CollapseBox,
Layout,
Message,
@ -121,12 +123,21 @@ const SettingsLanguagesView: FunctionComponent = () => {
Sonarr (or a Movie from Radarr) to find a matching Bazarr language
profile tag. It will use as the language profile the FIRST tag from
Sonarr/Radarr that matches the tag of a Bazarr language profile
EXACTLY. If mutiple tags match, there is no guarantee as to which one
EXACTLY. If multiple tags match, there is no guarantee as to which one
will be used, so choose your tag names carefully. Also, if you update
the tag names in Sonarr/Radarr, Bazarr will detect this and repeat the
matching process for the affected shows. However, if a show's only
matching tag is removed from Sonarr/Radarr, Bazarr will NOT remove the
show's existing language profile, but keep it, as is.
show's existing language profile for that reason. But if you wish to
have language profiles removed automatically by tag value, simply
enter a list of one or more tags in the{" "}
<MantineText fw={700} span>
Remove Profile Tags
</MantineText>{" "}
entry list below. If your video tag matches one of the tags in that
list, then Bazarr will remove the language profile for that video. If
there is a conflict between profile selection and profile removal,
then profile removal wins out and is performed.
</Message>
<Check
label="Series"
@ -136,6 +147,19 @@ const SettingsLanguagesView: FunctionComponent = () => {
label="Movies"
settingKey="settings-general-movie_tag_enabled"
></Check>
<Chips
label="Remove Profile Tags"
settingKey="settings-general-remove_profile_tags"
sanitizeFn={(values: string[] | null) =>
values?.map((item) =>
item.replace(/[^a-z0-9_-]/gi, "").toLowerCase(),
)
}
></Chips>
<Message>
Enter tag values that will trigger a language profile removal. Leave
empty if you don't want Bazarr to remove language profiles.
</Message>
</Section>
<Section header="Default Settings">
<Check

@ -1,5 +1,5 @@
import { FunctionComponent } from "react";
import { Code, Space, Table } from "@mantine/core";
import React, { FunctionComponent } from "react";
import { Code, Space, Table, Text as MantineText } from "@mantine/core";
import {
Check,
CollapseBox,
@ -115,14 +115,16 @@ const commandOptions: CommandOption[] = [
},
];
const commandOptionElements: JSX.Element[] = commandOptions.map((op, idx) => (
<tr key={idx}>
<td>
<Code>{op.option}</Code>
</td>
<td>{op.description}</td>
</tr>
));
const commandOptionElements: React.JSX.Element[] = commandOptions.map(
(op, idx) => (
<tr key={idx}>
<td>
<Code>{op.option}</Code>
</td>
<td>{op.description}</td>
</tr>
),
);
const SettingsSubtitlesView: FunctionComponent = () => {
return (
@ -436,8 +438,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-subsync-subsync_threshold"></Slider>
<Space />
<Message>
Only series subtitles with scores <b>below</b> this value will be
automatically synchronized.
Only series subtitles with scores{" "}
<MantineText fw={700} span>
below
</MantineText>{" "}
this value will be automatically synchronized.
</Message>
</CollapseBox>
<Check
@ -451,8 +456,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-subsync-subsync_movie_threshold"></Slider>
<Space />
<Message>
Only movie subtitles with scores <b>below</b> this value will be
automatically synchronized.
Only movie subtitles with scores{" "}
<MantineText fw={700} span>
below
</MantineText>{" "}
this value will be automatically synchronized.
</Message>
</CollapseBox>
</CollapseBox>
@ -478,8 +486,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-general-postprocessing_threshold"></Slider>
<Space />
<Message>
Only series subtitles with scores <b>below</b> this value will be
automatically post-processed.
Only series subtitles with scores{" "}
<MantineText fw={700} span>
below
</MantineText>{" "}
this value will be automatically post-processed.
</Message>
</CollapseBox>
<Check
@ -493,8 +504,11 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<Slider settingKey="settings-general-postprocessing_threshold_movie"></Slider>
<Space />
<Message>
Only movie subtitles with scores <b>below</b> this value will be
automatically post-processed.
Only movie subtitles with scores{" "}
<MantineText fw={700} span>
below
</MantineText>{" "}
this value will be automatically post-processed.
</Message>
</CollapseBox>
<Text

@ -2,7 +2,7 @@ import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
import { useForm } from "@mantine/form";
import { describe, it } from "vitest";
import { FormContext, FormValues } from "@/pages/Settings/utilities/FormValues";
import { render, RenderOptions, screen } from "@/tests";
import { render, screen } from "@/tests";
import { Number, Text } from "./forms";
const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
@ -15,10 +15,8 @@ const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
};
const formRender = (
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) => render(<FormSupport>{ui}</FormSupport>);
const formRender = (ui: ReactElement) =>
render(<FormSupport>{ui}</FormSupport>);
describe("Settings form", () => {
describe("number component", () => {

@ -1,4 +1,4 @@
import { FunctionComponent, ReactNode, ReactText } from "react";
import { FunctionComponent, ReactNode } from "react";
import {
Input,
NumberInput,
@ -49,7 +49,7 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
);
};
export type TextProps = BaseInput<ReactText> & TextInputProps;
export type TextProps = BaseInput<string | number> & TextInputProps;
export const Text: FunctionComponent<TextProps> = (props) => {
const { value, update, rest } = useBaseInput(props);
@ -86,11 +86,7 @@ export interface CheckProps extends BaseInput<boolean> {
inline?: boolean;
}
export const Check: FunctionComponent<CheckProps> = ({
label,
inline,
...props
}) => {
export const Check: FunctionComponent<CheckProps> = ({ label, ...props }) => {
const { value, update, rest } = useBaseInput(props);
return (

@ -62,6 +62,7 @@ declare namespace Settings {
postprocessing_cmd?: string;
postprocessing_threshold: number;
postprocessing_threshold_movie: number;
remove_profile_tags: string[];
single_language: boolean;
subfolder: string;
subfolder_custom?: string;

Loading…
Cancel
Save