diff --git a/bazarr/api/system/logs.py b/bazarr/api/system/logs.py index 606b900d5..51206ea11 100644 --- a/bazarr/api/system/logs.py +++ b/bazarr/api/system/logs.py @@ -2,9 +2,10 @@ import io import os +import re from flask_restx import Resource, Namespace, fields, marshal - +from app.config import settings from app.logger import empty_log from app.get_args import args @@ -29,12 +30,62 @@ class SystemLogs(Resource): def get(self): """List log entries""" logs = [] + include = str(settings.log.include_filter) + exclude = str(settings.log.exclude_filter) + ignore_case = settings.log.ignore_case + regex = settings.log.use_regex + if regex: + # pre-compile regular expressions for better performance + if ignore_case: + flags = re.IGNORECASE + else: + flags = 0 + if len(include) > 0: + try: + include_compiled = re.compile(include, flags) + except: + include_compiled = None + if len(exclude) > 0: + try: + exclude_compiled = re.compile(exclude, flags) + except: + exclude_compiled = None + elif ignore_case: + include = include.casefold() + exclude = exclude.casefold() + with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file: raw_lines = file.read() lines = raw_lines.split('|\n') for line in lines: if line == '': continue + if ignore_case and not regex: + compare_line = line.casefold() + else: + compare_line = line + if len(include) > 0: + if regex: + if include_compiled is None: + # if invalid re, keep the line + keep = True + else: + keep = include_compiled.search(compare_line) + else: + keep = include in compare_line + if not keep: + continue + if len(exclude) > 0: + if regex: + if exclude_compiled is None: + # if invalid re, keep the line + skip = False + else: + skip = exclude_compiled.search(compare_line) + else: + skip = exclude in compare_line + if skip: + continue raw_message = line.split('|') raw_message_len = len(raw_message) if raw_message_len > 3: diff --git a/bazarr/app/config.py b/bazarr/app/config.py index 3920969e2..bc1b64a2e 100644 --- a/bazarr/app/config.py +++ b/bazarr/app/config.py @@ -4,6 +4,7 @@ import hashlib import os import ast import logging +import re from urllib.parse import quote_plus from subliminal.cache import region @@ -122,6 +123,12 @@ validators = [ Validator('general.skip_hashing', must_exist=True, default=False, is_type_of=bool), Validator('general.language_equals', must_exist=True, default=[], is_type_of=list), + # log section + Validator('log.include_filter', must_exist=True, default='', is_type_of=str, cast=str), + Validator('log.exclude_filter', must_exist=True, default='', is_type_of=str, cast=str), + Validator('log.ignore_case', must_exist=True, default=False, is_type_of=bool), + Validator('log.use_regex', must_exist=True, default=False, is_type_of=bool), + # auth section Validator('auth.apikey', must_exist=True, default=hexlify(os.urandom(16)).decode(), is_type_of=str), Validator('auth.type', must_exist=True, default=None, is_type_of=(NoneType, str), @@ -430,7 +437,7 @@ array_keys = ['excluded_tags', empty_values = ['', 'None', 'null', 'undefined', None, []] -str_keys = ['chmod'] +str_keys = ['chmod', 'log_include_filter', 'log_exclude_filter'] # Increase Sonarr and Radarr sync interval since we now use SignalR feed to update in real time if settings.sonarr.series_sync < 15: @@ -477,6 +484,24 @@ def get_settings(): settings_to_return[k].update({subk: subv}) return settings_to_return +def validate_log_regex(): + # handle bug in dynaconf that changes strings to numbers, so change them back to str + if not isinstance(settings.log.include_filter, str): + settings.log.include_filter = str(settings.log.include_filter) + if not isinstance(settings.log.exclude_filter, str): + settings.log.exclude_filter = str(settings.log.exclude_filter) + + if (settings.log.use_regex): + # compile any regular expressions specified to see if they are valid + # if invalid, tell the user which one + try: + re.compile(settings.log.include_filter) + except: + raise ValidationError(f"Include filter: invalid regular expression: {settings.log.include_filter}") + try: + re.compile(settings.log.exclude_filter) + except: + raise ValidationError(f"Exclude filter: invalid regular expression: {settings.log.exclude_filter}") def save_settings(settings_items): configure_debug = False @@ -494,7 +519,8 @@ def save_settings(settings_items): undefined_subtitles_track_default_changed = False audio_tracks_parsing_changed = False reset_providers = False - + check_log_regex = False + # Subzero Mods update_subzero = False subzero_mods = get_array_from(settings.general.subzero_mods) @@ -630,12 +656,10 @@ def save_settings(settings_items): if key != settings.opensubtitlescom.username: reset_providers = True region.delete('oscom_token') - region.delete('oscom_server') elif key == 'settings-opensubtitlescom-password': if key != settings.opensubtitlescom.password: reset_providers = True region.delete('oscom_token') - region.delete('oscom_server') if key == 'settings-subscene-username': if key != settings.subscene.username: @@ -718,6 +742,7 @@ def save_settings(settings_items): try: settings.validators.validate() + validate_log_regex() except ValidationError: settings.reload() raise diff --git a/frontend/src/components/tables/PageControl.tsx b/frontend/src/components/tables/PageControl.tsx index d00edcef6..0767593de 100644 --- a/frontend/src/components/tables/PageControl.tsx +++ b/frontend/src/components/tables/PageControl.tsx @@ -1,6 +1,6 @@ import { useIsLoading } from "@/contexts"; import { Group, Pagination, Text } from "@mantine/core"; -import { FunctionComponent } from "react"; +import { FunctionComponent, useEffect } from "react"; interface Props { count: number; index: number; @@ -22,6 +22,11 @@ const PageControl: FunctionComponent = ({ const isLoading = useIsLoading(); + // Jump to first page if total page count changes + useEffect(() => { + goto(0); + }, [total, goto]); + return ( diff --git a/frontend/src/components/tables/PageTable.tsx b/frontend/src/components/tables/PageTable.tsx index fa958b28f..d84940857 100644 --- a/frontend/src/components/tables/PageTable.tsx +++ b/frontend/src/components/tables/PageTable.tsx @@ -1,4 +1,5 @@ import { ScrollToTop } from "@/utilities"; +import { usePageSize } from "@/utilities/storage"; import { useEffect } from "react"; import { usePagination, useTable } from "react-table"; import BaseTable from "./BaseTable"; @@ -22,6 +23,9 @@ export default function PageTable(props: Props) { ...(plugins ?? []) ); + // use page size as specified in UI settings + instance.state.pageSize = usePageSize(); + if (instanceRef) { instanceRef.current = instance; } diff --git a/frontend/src/pages/Settings/General/index.tsx b/frontend/src/pages/Settings/General/index.tsx index 96336a250..8cc5ea8c3 100644 --- a/frontend/src/pages/Settings/General/index.tsx +++ b/frontend/src/pages/Settings/General/index.tsx @@ -177,6 +177,22 @@ const SettingsGeneralView: FunctionComponent = () => {
Debug logging should only be enabled temporarily + + + +
void; +} + +const LayoutModal: FunctionComponent = (props) => { + const { children, callbackModal } = props; + + const { data: settings, isLoading, isRefetching } = useSystemSettings(); + const { mutate, isLoading: isMutating } = useSettingsMutation(); + + const form = useForm({ + initialValues: { + settings: {}, + hooks: {}, + }, + }); + + useOnValueChange(isRefetching, (value) => { + if (!value) { + form.reset(); + } + }); + + const submit = useCallback( + (values: FormValues) => { + const { settings, hooks } = values; + if (Object.keys(settings).length > 0) { + const settingsToSubmit = { ...settings }; + runHooks(hooks, settingsToSubmit); + LOG("info", "submitting settings", settingsToSubmit); + mutate(settingsToSubmit); + // wait for settings to be validated before callback + // let the user see the spinning indicator on the Save button before the modal closes + setTimeout(() => { + callbackModal(true); + }, 500); + } + }, + [mutate, callbackModal] + ); + + const totalStagedCount = useMemo(() => { + return Object.keys(form.values.settings).length; + }, [form.values.settings]); + + return ( + + +
+ + + + {children} + + + + + + + + + + +
+
+ ); +}; + +export default LayoutModal; diff --git a/frontend/src/pages/Settings/components/index.tsx b/frontend/src/pages/Settings/components/index.tsx index 15b60db29..9502f4366 100644 --- a/frontend/src/pages/Settings/components/index.tsx +++ b/frontend/src/pages/Settings/components/index.tsx @@ -65,6 +65,7 @@ export const URLTestButton: FunctionComponent<{ export * from "./Card"; export * from "./Layout"; export { default as Layout } from "./Layout"; +export { default as LayoutModal } from "./LayoutModal"; export * from "./Message"; export * from "./Section"; export * from "./collapse"; diff --git a/frontend/src/pages/System/Logs/index.tsx b/frontend/src/pages/System/Logs/index.tsx index 8d12847d7..d77e102d8 100644 --- a/frontend/src/pages/System/Logs/index.tsx +++ b/frontend/src/pages/System/Logs/index.tsx @@ -1,10 +1,17 @@ -import { useDeleteLogs, useSystemLogs } from "@/apis/hooks"; +import { useDeleteLogs, useSystemLogs, useSystemSettings } from "@/apis/hooks"; import { Toolbox } from "@/components"; import { QueryOverlay } from "@/components/async"; +import { Check, LayoutModal, Message, Text } from "@/pages/Settings/components"; import { Environment } from "@/utilities"; -import { faDownload, faSync, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { Container, Group } from "@mantine/core"; +import { + faDownload, + faFilter, + faSync, + faTrash, +} from "@fortawesome/free-solid-svg-icons"; +import { Badge, Container, Group, Stack } from "@mantine/core"; import { useDocumentTitle } from "@mantine/hooks"; +import { useModals } from "@mantine/modals"; import { FunctionComponent, useCallback } from "react"; import Table from "./table"; @@ -20,6 +27,61 @@ const SystemLogsView: FunctionComponent = () => { useDocumentTitle("Logs - Bazarr (System)"); + const { data: settings } = useSystemSettings(); + const modals = useModals(); + + const suffix = () => { + const include = settings?.log.include_filter; + const exclude = settings?.log.exclude_filter; + const includeIndex = include !== "" && include !== undefined ? 1 : 0; + const excludeIndex = exclude !== "" && exclude !== undefined ? 1 : 0; + const filters = [ + ["", "I"], + ["E", "I/E"], + ]; + const filterStr = filters[excludeIndex][includeIndex]; + const debugStr = settings?.general.debug ? "Debug" : ""; + const spaceStr = debugStr !== "" && filterStr !== "" ? " " : ""; + const suffixStr = debugStr + spaceStr + filterStr; + return suffixStr; + }; + + const openFilterModal = () => { + const callbackModal = (close: boolean) => { + if (close) { + modals.closeModal(id); + } + }; + + const id = modals.openModal({ + title: "Set Log Debug and Filter Options", + children: ( + + + + Debug logging should only be enabled temporarily + + + + + + + ), + }); + }; + return ( @@ -42,6 +104,22 @@ const SystemLogsView: FunctionComponent = () => { > Empty + + {suffix()} + + ) : ( + <> + ) + } + > + Filter +
diff --git a/frontend/src/types/settings.d.ts b/frontend/src/types/settings.d.ts index d88489a0e..d46a9734e 100644 --- a/frontend/src/types/settings.d.ts +++ b/frontend/src/types/settings.d.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ interface Settings { general: Settings.General; + log: Settings.Log; proxy: Settings.Proxy; auth: Settings.Auth; subsync: Settings.Subsync; @@ -82,6 +83,13 @@ declare namespace Settings { wanted_search_frequency_movie: number; } + interface Log { + include_filter: string; + exclude_filter: string; + ignore_case: boolean; + use_regex: boolean; + } + interface Proxy { exclude: string[]; type?: string;