From 678b2f0e555e4cc431c61a83907b2a656c8ef25f Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Sun, 9 May 2021 21:23:58 +0800 Subject: [PATCH] Add background task notification --- bazarr/event_handler.py | 4 -- bazarr/get_episodes.py | 4 +- bazarr/get_movies.py | 4 +- bazarr/get_series.py | 4 +- bazarr/get_subtitle.py | 14 +++-- bazarr/list_subtitles.py | 6 +-- frontend/src/@redux/actions/factory.ts | 6 --- frontend/src/@redux/actions/movie.ts | 3 +- frontend/src/@redux/actions/series.ts | 3 +- frontend/src/@redux/hooks/index.ts | 65 ++++++++++++++++++----- frontend/src/@redux/reducers/site.ts | 4 +- frontend/src/@socketio/hooks.ts | 19 ++----- frontend/src/@socketio/index.ts | 4 +- frontend/src/@socketio/reducer.ts | 20 ++++++- frontend/src/@types/socket.d.ts | 66 ++++++++++++++---------- frontend/src/App/notifications/index.tsx | 31 ++++++++--- 16 files changed, 157 insertions(+), 100 deletions(-) diff --git a/bazarr/event_handler.py b/bazarr/event_handler.py index c50abe72a..1924b161f 100644 --- a/bazarr/event_handler.py +++ b/bazarr/event_handler.py @@ -25,7 +25,3 @@ def show_message(msg): def show_progress(id, name, value, count): event_stream(type="progress", payload={"id": id, "name": name, "value": value, "count": count}) - - -def hide_progress(id, delay=3): - event_stream(type="progress", action="delete", payload={"id": id, "delay": delay}) diff --git a/bazarr/get_episodes.py b/bazarr/get_episodes.py index c0eaf3a7e..3bcbfd19c 100644 --- a/bazarr/get_episodes.py +++ b/bazarr/get_episodes.py @@ -9,7 +9,7 @@ from config import settings, url_sonarr from helper import path_mappings from list_subtitles import store_subtitles, series_full_scan_subtitles from get_subtitle import episode_download_subtitles -from event_handler import event_stream, show_progress, hide_progress +from event_handler import event_stream, show_progress headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} @@ -63,8 +63,6 @@ def sync_episodes(): else: episodes_to_add.append(episodeParser(episode)) - hide_progress(id='episodes_progress') - # Remove old episodes from DB removed_episodes = list(set(current_episodes_db_list) - set(current_episodes_sonarr)) diff --git a/bazarr/get_movies.py b/bazarr/get_movies.py index 9f7aea8b2..4a43a5c6b 100644 --- a/bazarr/get_movies.py +++ b/bazarr/get_movies.py @@ -12,7 +12,7 @@ from get_rootfolder import check_radarr_rootfolder from get_subtitle import movies_download_subtitles from database import database, dict_converter, get_exclusion_clause -from event_handler import event_stream, show_progress, hide_progress +from event_handler import event_stream, show_progress headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} @@ -87,8 +87,6 @@ def update_movies(): movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)) - hide_progress(id='movies_progress') - # Remove old movies from DB removed_movies = list(set(current_movies_db_list) - set(current_movies_radarr)) diff --git a/bazarr/get_series.py b/bazarr/get_series.py index a0dea56fc..5d286b095 100644 --- a/bazarr/get_series.py +++ b/bazarr/get_series.py @@ -10,7 +10,7 @@ from get_rootfolder import check_sonarr_rootfolder from database import database, dict_converter from utils import get_sonarr_version from helper import path_mappings -from event_handler import event_stream, show_progress, hide_progress +from event_handler import event_stream, show_progress headers = {"User-Agent": os.environ["SZ_USER_AGENT"]} @@ -66,8 +66,6 @@ def update_series(): tags_dict=tagsDict, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles)) - hide_progress(id='series_progress') - # Remove old series from DB removed_series = list(set(current_shows_db_list) - set(current_shows_sonarr)) diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index 0d08bee0e..74c73def5 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -33,7 +33,7 @@ from subsyncer import subsync from guessit import guessit from database import database, dict_mapper, get_exclusion_clause, get_profiles_list, get_audio_profile_languages, \ get_desired_languages -from event_handler import event_stream, show_progress, hide_progress +from event_handler import event_stream, show_progress, from embedded_subs_reader import parse_video_metadata from analytics import track_event @@ -806,7 +806,7 @@ def series_download_subtitles(no): logging.info("BAZARR All providers are throttled") break - hide_progress(id='series_search_progress_{}'.format(no)) + def episode_download_subtitles(no): @@ -929,8 +929,6 @@ def movies_download_subtitles(no): logging.info("BAZARR All providers are throttled") break - hide_progress(id='movie_search_progress_{}'.format(no)) - def wanted_download_subtitles(path): episodes_details = database.execute("SELECT table_episodes.path, table_episodes.missing_subtitles, " @@ -1100,7 +1098,7 @@ def wanted_search_missing_subtitles_series(): logging.info("BAZARR All providers are throttled") return - hide_progress(id='wanted_episodes_progress') + logging.info('BAZARR Finished searching for missing Series Subtitles. Check History for more information.') @@ -1124,7 +1122,7 @@ def wanted_search_missing_subtitles_movies(): logging.info("BAZARR All providers are throttled") return - hide_progress(id='wanted_movies_progress') + logging.info('BAZARR Finished searching for missing Movies Subtitles. Check History for more information.') @@ -1402,7 +1400,7 @@ def upgrade_subtitles(): language_code, provider, score, subs_id, subs_path) send_notifications(episode['sonarrSeriesId'], episode['sonarrEpisodeId'], message) - hide_progress('upgrade_episodes_progress') + if settings.general.getboolean('use_radarr'): for i, movie in enumerate(movies_to_upgrade, 1): @@ -1468,7 +1466,7 @@ def upgrade_subtitles(): history_log_movie(3, movie['radarrId'], message, path, language_code, provider, score, subs_id, subs_path) send_notifications_movie(movie['radarrId'], message) - hide_progress(id='upgrade_movies_progress') + logging.info('BAZARR Finished searching for Subtitles to upgrade. Check History for more information.') diff --git a/bazarr/list_subtitles.py b/bazarr/list_subtitles.py index 65fed2f60..8efeb17f5 100644 --- a/bazarr/list_subtitles.py +++ b/bazarr/list_subtitles.py @@ -15,7 +15,7 @@ from config import settings from helper import path_mappings, get_subtitle_destination_folder from embedded_subs_reader import embedded_subs_reader -from event_handler import event_stream, show_progress, hide_progress +from event_handler import event_stream, show_progress from charamel import Detector gc.enable() @@ -488,8 +488,6 @@ def series_full_scan_subtitles(): value=i, count=count_episodes) store_subtitles(episode['path'], path_mappings.path_replace(episode['path'])) - - hide_progress(id='episodes_disk_scan') gc.collect() @@ -505,8 +503,6 @@ def movies_full_scan_subtitles(): count=count_movies) store_subtitles_movie(movie['path'], path_mappings.path_replace_movie(movie['path'])) - hide_progress(id='movies_disk_scan') - gc.collect() diff --git a/frontend/src/@redux/actions/factory.ts b/frontend/src/@redux/actions/factory.ts index 22a24e1f3..9f502a604 100644 --- a/frontend/src/@redux/actions/factory.ts +++ b/frontend/src/@redux/actions/factory.ts @@ -1,4 +1,3 @@ -import { createAction } from "redux-actions"; import { ActionCallback, ActionDispatcher, @@ -122,8 +121,3 @@ export function createCallbackAction( return (...args: Parameters) => callbackActionFactory(fn(args), success, error); } - -// Helper -export function createDeleteAction(type: string): SocketIO.ActionFn { - return createAction(type, (id?: number[]) => id ?? []); -} diff --git a/frontend/src/@redux/actions/movie.ts b/frontend/src/@redux/actions/movie.ts index 8de392181..b76752cdc 100644 --- a/frontend/src/@redux/actions/movie.ts +++ b/frontend/src/@redux/actions/movie.ts @@ -1,3 +1,4 @@ +import { createDeleteAction } from "../../@socketio/reducer"; import { MoviesApi } from "../../apis"; import { MOVIES_DELETE_ITEMS, @@ -7,7 +8,7 @@ import { MOVIES_UPDATE_LIST, MOVIES_UPDATE_WANTED_LIST, } from "../constants"; -import { createAsyncAction, createDeleteAction } from "./factory"; +import { createAsyncAction } from "./factory"; export const movieUpdateList = createAsyncAction( MOVIES_UPDATE_LIST, diff --git a/frontend/src/@redux/actions/series.ts b/frontend/src/@redux/actions/series.ts index bbee5b3b5..c53277469 100644 --- a/frontend/src/@redux/actions/series.ts +++ b/frontend/src/@redux/actions/series.ts @@ -1,3 +1,4 @@ +import { createDeleteAction } from "../../@socketio/reducer"; import { EpisodesApi, SeriesApi } from "../../apis"; import { SERIES_DELETE_EPISODES, @@ -9,7 +10,7 @@ import { SERIES_UPDATE_LIST, SERIES_UPDATE_WANTED_LIST, } from "../constants"; -import { createAsyncAction, createDeleteAction } from "./factory"; +import { createAsyncAction } from "./factory"; export const seriesUpdateWantedList = createAsyncAction( SERIES_UPDATE_WANTED_LIST, diff --git a/frontend/src/@redux/hooks/index.ts b/frontend/src/@redux/hooks/index.ts index 1bd6062f6..bd89c49b6 100644 --- a/frontend/src/@redux/hooks/index.ts +++ b/frontend/src/@redux/hooks/index.ts @@ -54,7 +54,10 @@ export function useSystemLogs() { export function useSystemTasks() { const items = useReduxStore((s) => s.system.tasks); const update = useReduxAction(systemUpdateTasks); - useSocketIOReducer("task", update); + const reducer = useMemo(() => ({ key: "task", update }), [ + update, + ]); + useSocketIOReducer(reducer); useEffect(() => { update(); @@ -245,10 +248,18 @@ export function useEpisodesBy(seriesId?: number) { const actionById = useReduxAction(episodeUpdateById); const wrapActionById = useWrapToOptionalId(actionById); const deleteAction = useReduxAction(episodeDeleteItems); - useSocketIOReducer("episode", undefined, wrapActionById, deleteAction); + const episodeReducer = useMemo( + () => ({ key: "episode", update: wrapActionById, delete: deleteAction }), + [wrapActionById, deleteAction] + ); + useSocketIOReducer(episodeReducer); const wrapAction = useWrapToOptionalId(action); - useSocketIOReducer("series", undefined, wrapAction); + const seriesReducer = useMemo( + () => ({ key: "series", update: wrapAction }), + [wrapAction] + ); + useSocketIOReducer(seriesReducer); useEffect(() => { update(); @@ -315,7 +326,15 @@ export function useWantedSeries() { const updateAction = useWrapToOptionalId(update); const deleteAction = useReduxAction(seriesDeleteWantedItems); - useSocketIOReducer("episode-wanted", undefined, updateAction, deleteAction); + const reducer = useMemo( + () => ({ + key: "episode-wanted", + update: updateAction, + delete: deleteAction, + }), + [updateAction, deleteAction] + ); + useSocketIOReducer(reducer); return stateBuilder(items, update); } @@ -326,7 +345,15 @@ export function useWantedMovies() { const updateAction = useWrapToOptionalId(update); const deleteAction = useReduxAction(movieDeleteWantedItems); - useSocketIOReducer("movie-wanted", undefined, updateAction, deleteAction); + const reducer = useMemo( + () => ({ + key: "movie-wanted", + update: updateAction, + delete: deleteAction, + }), + [updateAction, deleteAction] + ); + useSocketIOReducer(reducer); return stateBuilder(items, update); } @@ -334,8 +361,11 @@ export function useWantedMovies() { export function useBlacklistMovies() { const update = useReduxAction(movieUpdateBlacklist); const items = useReduxStore((d) => d.movie.blacklist); - - useSocketIOReducer("movie-blacklist", update); + const reducer = useMemo( + () => ({ key: "movie-blacklist", update }), + [update] + ); + useSocketIOReducer(reducer); useEffect(() => { update(); @@ -346,8 +376,11 @@ export function useBlacklistMovies() { export function useBlacklistSeries() { const update = useReduxAction(seriesUpdateBlacklist); const items = useReduxStore((d) => d.series.blacklist); - - useSocketIOReducer("episode-blacklist", update); + const reducer = useMemo( + () => ({ key: "episode-blacklist", update }), + [update] + ); + useSocketIOReducer(reducer); useEffect(() => { update(); @@ -358,8 +391,11 @@ export function useBlacklistSeries() { export function useMoviesHistory() { const update = useReduxAction(movieUpdateHistoryList); const items = useReduxStore((s) => s.movie.historyList); - - useSocketIOReducer("movie-history", update); + const reducer = useMemo( + () => ({ key: "movie-history", update }), + [update] + ); + useSocketIOReducer(reducer); useEffect(() => { update(); @@ -370,8 +406,11 @@ export function useMoviesHistory() { export function useSeriesHistory() { const update = useReduxAction(seriesUpdateHistoryList); const items = useReduxStore((s) => s.series.historyList); - - useSocketIOReducer("episode-history", update); + const reducer = useMemo( + () => ({ key: "episode-history", update }), + [update] + ); + useSocketIOReducer(reducer); useEffect(() => { update(); diff --git a/frontend/src/@redux/reducers/site.ts b/frontend/src/@redux/reducers/site.ts index 660f192a9..c7eac7737 100644 --- a/frontend/src/@redux/reducers/site.ts +++ b/frontend/src/@redux/reducers/site.ts @@ -39,7 +39,7 @@ const reducer = handleActions( action: Action ) => { const notifications = uniqBy( - [...action.payload, ...state.notifications], + [...action.payload.reverse(), ...state.notifications], (n) => n.id ); return { ...state, notifications }; @@ -51,7 +51,7 @@ const reducer = handleActions( }, [SITE_PROGRESS_ADD]: (state, action: Action) => { const progress = uniqBy( - [...action.payload, ...state.progress], + [...action.payload.reverse(), ...state.progress], (n) => n.id ); return { ...state, progress }; diff --git a/frontend/src/@socketio/hooks.ts b/frontend/src/@socketio/hooks.ts index 802907ea1..efdc11cd9 100644 --- a/frontend/src/@socketio/hooks.ts +++ b/frontend/src/@socketio/hooks.ts @@ -1,29 +1,20 @@ -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect } from "react"; import Socketio from "."; import { log } from "../utilites/logger"; -export function useSocketIOReducer( - key: SocketIO.EventType, - any?: () => void, - update?: SocketIO.ActionFn, - remove?: SocketIO.ActionFn -) { - const reducer = useMemo( - () => ({ key, any, update, delete: remove }), - [key, any, update, remove] - ); +export function useSocketIOReducer(reducer: SocketIO.Reducer) { useEffect(() => { Socketio.addReducer(reducer); - log("info", "listening to SocketIO event", key); + log("info", "listening to SocketIO event", reducer.key); return () => { Socketio.removeReducer(reducer); }; - }, [reducer, key]); + }, [reducer]); } export function useWrapToOptionalId( fn: (id: number[]) => void -): SocketIO.ActionFn { +): SocketIO.ActionFn { return useCallback( (id?: number[]) => { if (id) { diff --git a/frontend/src/@socketio/index.ts b/frontend/src/@socketio/index.ts index ea109e681..44419ef7d 100644 --- a/frontend/src/@socketio/index.ts +++ b/frontend/src/@socketio/index.ts @@ -27,7 +27,7 @@ class SocketIOClient { this.socket.on("data", this.onEvent.bind(this)); this.events = []; - this.debounceReduce = debounce(this.reduce, 200); + this.debounceReduce = debounce(this.reduce, 20); this.reducers = []; } @@ -91,7 +91,7 @@ class SocketIOClient { forIn(element, (ids, key) => { ids = uniq(ids); - const action = handler[key as SocketIO.ActionType]; + const action = handler[key as SocketIO.Action]; if (action) { action(ids); } else if (anyAction === undefined) { diff --git a/frontend/src/@socketio/reducer.ts b/frontend/src/@socketio/reducer.ts index 7d686b4d1..a6506832f 100644 --- a/frontend/src/@socketio/reducer.ts +++ b/frontend/src/@socketio/reducer.ts @@ -1,3 +1,4 @@ +import { createAction } from "redux-actions"; import { badgeUpdateAll, bootstrap, @@ -6,6 +7,7 @@ import { seriesDeleteItems, seriesUpdateList, siteAddNotifications, + siteAddProgress, siteInitializationFailed, siteUpdateOffline, systemUpdateLanguagesAll, @@ -13,10 +15,16 @@ import { } from "../@redux/actions"; import reduxStore from "../@redux/store"; -function bindToReduxStore(fn: (ids?: number[]) => any): SocketIO.ActionFn { +function bindToReduxStore( + fn: (ids?: number[]) => any +): SocketIO.ActionFn { return (ids?: number[]) => reduxStore.dispatch(fn(ids)); } +export function createDeleteAction(type: string): SocketIO.ActionFn { + return createAction(type, (id?: number[]) => id ?? []); +} + export function createDefaultReducer(): SocketIO.Reducer[] { return [ { @@ -44,7 +52,7 @@ export function createDefaultReducer(): SocketIO.Reducer[] { }, { key: "message", - update: (msg?: string[]) => { + update: (msg) => { if (msg) { const notifications = msg.map((message) => ({ message, @@ -57,6 +65,14 @@ export function createDefaultReducer(): SocketIO.Reducer[] { } }, }, + { + key: "progress", + update: (progress) => { + if (progress) { + reduxStore.dispatch(siteAddProgress(progress)); + } + }, + }, { key: "series", update: bindToReduxStore(seriesUpdateList), diff --git a/frontend/src/@types/socket.d.ts b/frontend/src/@types/socket.d.ts index aac73de01..77ca6d76b 100644 --- a/frontend/src/@types/socket.d.ts +++ b/frontend/src/@types/socket.d.ts @@ -1,43 +1,55 @@ namespace SocketIO { - type EventType = - | "connect" - | "connect_error" - | "disconnect" + type Action = "update" | "delete"; + + type EventType = NumEventType | NullEventType | SpecialEventType; + + type NumEventType = | "movie" | "series" | "episode" - | "episode-history" - | "episode-blacklist" | "episode-wanted" - | "movie-history" + | "movie-wanted"; + + type NullEventType = + | "connect" + | "connect_error" + | "disconnect" + | "episode-blacklist" + | "episode-history" | "movie-blacklist" - | "movie-wanted" + | "movie-history" | "badges" | "task" | "settings" - | "languages" - | "message"; + | "languages"; - type ActionType = "update" | "delete"; + type SpecialEventType = "message" | "progress"; - interface Event { - type: EventType; - action: ActionType; - payload: any; // TODO: Use specific types - } - - type ActionFn = (payload?: any[]) => void; - - type Reducer = { - key: EventType; - any?: () => any; - } & Partial>; - - type ActionRecord = OptionalRecord< - EventType, - OptionalRecord + type ReducerCreator = ValueOf< + { + [P in E]: { + key: P; + any?: () => void; + } & Partial>>; + } >; + type Event = { + type: EventType; + action: Action; + payload: any; + }; + + type ActionFn = (payload?: T[]) => void; + + type Reducer = + | ReducerCreator + | ReducerCreator + | ReducerCreator<"message", string> + | ReducerCreator<"progress", CustomEvent.Progress>; + + type ActionRecord = OptionalRecord>; + namespace CustomEvent { type Progress = ReduxStore.Progress; } diff --git a/frontend/src/App/notifications/index.tsx b/frontend/src/App/notifications/index.tsx index c347b2274..0193eae2b 100644 --- a/frontend/src/App/notifications/index.tsx +++ b/frontend/src/App/notifications/index.tsx @@ -4,9 +4,13 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { capitalize } from "lodash"; -import React, { FunctionComponent, useCallback, useMemo } from "react"; +import React, { + FunctionComponent, + useCallback, + useEffect, + useMemo, +} from "react"; import { ProgressBar, Toast } from "react-bootstrap"; -import { useTimeoutWhen } from "rooks"; import { siteRemoveNotifications, siteRemoveProgress, @@ -48,7 +52,12 @@ const NotificationToast: FunctionComponent = (props) => { id, ]); - useTimeoutWhen(remove, timeout); + useEffect(() => { + const handle = setTimeout(remove, timeout); + return () => { + clearTimeout(handle); + }; + }, [props, remove, timeout]); return ( @@ -75,12 +84,22 @@ const ProgressToast: FunctionComponent = ({ const removeProgress = useReduxAction(siteRemoveProgress); const remove = useCallback(() => removeProgress(id), [removeProgress, id]); - // TODO: Auto remove + useEffect(() => { + const handle = setTimeout(remove, 5 * 1000); + return () => { + clearTimeout(handle); + }; + }, [value, remove]); + + const incomplete = value / count < 1; return ( + -
+
= ({