diff --git a/frontend/src/Movies/Detail/index.tsx b/frontend/src/Movies/Detail/index.tsx index 29774d95e..94b8a61f0 100644 --- a/frontend/src/Movies/Detail/index.tsx +++ b/frontend/src/Movies/Detail/index.tsx @@ -11,7 +11,7 @@ import React, { FunctionComponent, useState } from "react"; import { Alert, Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import { Redirect, RouteComponentProps, withRouter } from "react-router-dom"; -import { useIsGroupTaskRunningWithId } from "../../@modules/task/hooks"; +import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks"; import { useMovieBy, useProfileBy } from "../../@redux/hooks"; import { MoviesApi, ProvidersApi } from "../../apis"; import { @@ -24,7 +24,6 @@ import { useShowModal, } from "../../components"; import { ManualSearchModal } from "../../components/modals/ManualSearchModal"; -import { TaskGroupName } from "../../components/modals/MovieUploadModal"; import ItemOverview from "../../generic/ItemOverview"; import { RouterEmptyPath } from "../../special-pages/404"; import { useOnLoadedOnce } from "../../utilites"; @@ -59,7 +58,7 @@ const MovieDetailView: FunctionComponent = ({ match }) => { const [valid, setValid] = useState(true); - const hasTask = useIsGroupTaskRunningWithId(TaskGroupName, id); + const hasTask = useIsAnyTaskRunningWithId(id); useOnLoadedOnce(() => { if (movie.content === null) { diff --git a/frontend/src/Movies/Detail/table.tsx b/frontend/src/Movies/Detail/table.tsx index 021b1659e..5f86587f8 100644 --- a/frontend/src/Movies/Detail/table.tsx +++ b/frontend/src/Movies/Detail/table.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { FunctionComponent, useMemo } from "react"; import { Badge } from "react-bootstrap"; import { Column } from "react-table"; +import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks"; import { useProfileItemsToLanguages } from "../../@redux/hooks"; import { useShowOnlyDesired } from "../../@redux/hooks/site"; import { MoviesApi } from "../../apis"; @@ -21,6 +22,8 @@ const Table: FunctionComponent = ({ movie, profile }) => { const profileItems = useProfileItemsToLanguages(profile); + const hasTask = useIsAnyTaskRunningWithId(movie.radarrId); + const columns: Column[] = useMemo[]>( () => [ { @@ -64,6 +67,7 @@ const Table: FunctionComponent = ({ movie, profile }) => { } else if (original.path === missingText) { return ( MoviesApi.downloadSubtitles(movie.radarrId, { language: original.code2, @@ -80,6 +84,7 @@ const Table: FunctionComponent = ({ movie, profile }) => { } else { return ( @@ -98,7 +103,7 @@ const Table: FunctionComponent = ({ movie, profile }) => { }, }, ], - [movie] + [movie, hasTask] ); const data: Subtitle[] = useMemo(() => { diff --git a/frontend/src/Series/Episodes/index.tsx b/frontend/src/Series/Episodes/index.tsx index bf21b0a0c..f279a35c2 100644 --- a/frontend/src/Series/Episodes/index.tsx +++ b/frontend/src/Series/Episodes/index.tsx @@ -8,9 +8,10 @@ import { faWrench, } from "@fortawesome/free-solid-svg-icons"; import React, { FunctionComponent, useMemo, useState } from "react"; -import { Container, Row } from "react-bootstrap"; +import { Alert, Container, Row } from "react-bootstrap"; import { Helmet } from "react-helmet"; import { Redirect, RouteComponentProps, withRouter } from "react-router-dom"; +import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks"; import { useEpisodesBy, useProfileBy, useSerieBy } from "../../@redux/hooks"; import { SeriesApi } from "../../apis"; import { @@ -66,6 +67,8 @@ const SeriesEpisodesView: FunctionComponent = (props) => { const profile = useProfileBy(series.content?.profileId); + const hasTask = useIsAnyTaskRunningWithId(id); + if (isNaN(id) || !valid) { return ; } @@ -83,7 +86,7 @@ const SeriesEpisodesView: FunctionComponent = (props) => { SeriesApi.action({ action: "scan-disk", seriesid: id }) } @@ -98,7 +101,8 @@ const SeriesEpisodesView: FunctionComponent = (props) => { disabled={ serie.episodeFileCount === 0 || serie.profileId === null || - !available + !available || + hasTask } > Search @@ -106,7 +110,7 @@ const SeriesEpisodesView: FunctionComponent = (props) => { showModal("tools", episodes.content)} > @@ -125,12 +129,23 @@ const SeriesEpisodesView: FunctionComponent = (props) => { showModal("edit", serie)} > Edit Series + + + A background task is running for this show, actions are unavailable + + diff --git a/frontend/src/Series/Episodes/table.tsx b/frontend/src/Series/Episodes/table.tsx index ca7a4f00e..987cab835 100644 --- a/frontend/src/Series/Episodes/table.tsx +++ b/frontend/src/Series/Episodes/table.tsx @@ -9,6 +9,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { FunctionComponent, useCallback, useMemo } from "react"; import { Badge, ButtonGroup } from "react-bootstrap"; import { Column, TableUpdater } from "react-table"; +import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks"; import { useProfileItemsToLanguages } from "../../@redux/hooks"; import { useShowOnlyDesired } from "../../@redux/hooks/site"; import { ProvidersApi } from "../../apis"; @@ -54,6 +55,10 @@ const Table: FunctionComponent = ({ serie, episodes, profile }) => { const profileItems = useProfileItemsToLanguages(profile); + const hasTask = useIsAnyTaskRunningWithId( + serie.content?.sonarrSeriesId ?? -1 + ); + const columns: Column[] = useMemo[]>( () => [ { @@ -101,7 +106,7 @@ const Table: FunctionComponent = ({ serie, episodes, profile }) => { { Header: "Subtitles", accessor: "missing_subtitles", - Cell: ({ row, loose }) => { + Cell: ({ row }) => { const episode = row.original; const seriesid = episode.sonarrSeriesId; @@ -147,7 +152,7 @@ const Table: FunctionComponent = ({ serie, episodes, profile }) => { { externalUpdate && externalUpdate(row, "manual-search"); }} @@ -160,6 +165,7 @@ const Table: FunctionComponent = ({ serie, episodes, profile }) => { > { externalUpdate && externalUpdate(row, "tools"); }} @@ -169,7 +175,7 @@ const Table: FunctionComponent = ({ serie, episodes, profile }) => { }, }, ], - [onlyDesired, profileItems, serie] + [onlyDesired, profileItems, serie, hasTask] ); const updateRow = useCallback>( diff --git a/frontend/src/components/modals/MovieUploadModal.tsx b/frontend/src/components/modals/MovieUploadModal.tsx index 75cac7475..0d608f0c8 100644 --- a/frontend/src/components/modals/MovieUploadModal.tsx +++ b/frontend/src/components/modals/MovieUploadModal.tsx @@ -12,7 +12,7 @@ import { MoviesApi } from "../../apis"; import BaseModal, { BaseModalProps } from "./BaseModal"; import { useModalInformation } from "./hooks"; -export const TaskGroupName = "Uploading Movie Subtitles..."; +export const TaskGroupName = "Uploading Subtitles..."; const MovieUploadModal: FunctionComponent = (props) => { const modal = props; diff --git a/frontend/src/components/modals/SeriesUploadModal.tsx b/frontend/src/components/modals/SeriesUploadModal.tsx index 097d7300c..8e62247cd 100644 --- a/frontend/src/components/modals/SeriesUploadModal.tsx +++ b/frontend/src/components/modals/SeriesUploadModal.tsx @@ -1,7 +1,6 @@ import { faCheck, faCircleNotch, - faExclamationTriangle, faInfoCircle, faTrash, } from "@fortawesome/free-solid-svg-icons"; @@ -22,6 +21,8 @@ import { MessageIcon, SimpleTable, } from ".."; +import BackgroundTask from "../../@modules/task"; +import { createTask } from "../../@modules/task/utilites"; import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks"; import { EpisodesApi, SubtitlesApi } from "../../apis"; import { Selector } from "../inputs"; @@ -29,27 +30,17 @@ import BaseModal, { BaseModalProps } from "./BaseModal"; import { useModalInformation } from "./hooks"; enum State { - Update, + Updating, Valid, Warning, - Error, } interface PendingSubtitle { file: File; - didCheck: boolean; + state: State; instance?: Item.Episode; } -type SubtitleState = { - state: State; - infos: string[]; -}; - -type ProcessState = { - [name: string]: SubtitleState; -}; - type EpisodeMap = { [name: string]: Item.Episode; }; @@ -58,6 +49,8 @@ interface SerieProps { episodes: readonly Item.Episode[]; } +export const TaskGroupName = "Uploading Subtitles..."; + const SeriesUploadModal: FunctionComponent = ({ episodes, ...modal @@ -70,8 +63,6 @@ const SeriesUploadModal: FunctionComponent = ({ const [pending, setPending] = useState([]); - const [processState, setProcessState] = useState({}); - const profile = useProfileBy(payload?.profileId); const avaliableLanguages = useProfileItemsToLanguages(profile); @@ -86,38 +77,6 @@ const SeriesUploadModal: FunctionComponent = ({ const filelist = useMemo(() => pending.map((v) => v.file), [pending]); - // Vaildate - useEffect(() => { - const states = pending.reduce((prev, info) => { - const subState: SubtitleState = { - state: State.Valid, - infos: [], - }; - - const { file, instance } = info; - - if (!info.didCheck) { - subState.state = State.Update; - } else if (!instance) { - subState.infos.push("Season or episode info is missing"); - subState.state = State.Error; - } else { - if ( - instance.subtitles.find((v) => v.code2 === language?.code2) !== - undefined - ) { - subState.infos.push("Overwrite existing subtitle"); - subState.state = State.Warning; - } - } - - prev[file.name] = subState; - return prev; - }, {}); - - setProcessState(states); - }, [pending, language?.code2]); - const checkEpisodes = useCallback( async (list: PendingSubtitle[]) => { const names = list.map((v) => v.file.name); @@ -138,7 +97,7 @@ const SeriesUploadModal: FunctionComponent = ({ setPending((pd) => pd.map((v) => ({ ...v, - didCheck: true, + state: State.Valid, instance: episodeMap[v.file.name], })) ); @@ -154,18 +113,10 @@ const SeriesUploadModal: FunctionComponent = ({ return { file: f, didCheck: false, + state: State.Updating, }; }); setPending(list); - - const states = files.reduce( - (v, curr) => ({ - ...v, - [curr.name]: { state: State.Update, infos: [] }, - }), - {} - ); - setProcessState(states); checkEpisodes(list); }, [checkEpisodes] @@ -177,51 +128,31 @@ const SeriesUploadModal: FunctionComponent = ({ } const { sonarrSeriesId: seriesid } = payload; + const { code2, hi, forced } = language; - let uploadStates = pending.reduce((prev, curr) => { - prev[curr.file.name] = { state: State.Update, infos: [] }; - return prev; - }, {}); + const tasks = pending + .filter((v) => v.instance !== undefined) + .map((v) => { + const { sonarrEpisodeId: episodeid } = v.instance!; - setProcessState(uploadStates); + const form: FormType.UploadSubtitle = { + file: v.file, + language: code2, + hi: hi ?? false, + forced: forced ?? false, + }; - let exception = false; + return createTask( + v.file.name, + seriesid, + EpisodesApi.uploadSubtitles.bind(EpisodesApi), + seriesid, + episodeid, + form + ); + }); - for (const info of pending) { - if (info.instance) { - const { sonarrEpisodeId: episodeid } = info.instance; - const { file } = info; - const { code2, hi, forced } = language; - - try { - const form: FormType.UploadSubtitle = { - file, - language: code2, - hi: hi ?? false, - forced: forced ?? false, - }; - - await EpisodesApi.uploadSubtitles(seriesid, episodeid, form); - - uploadStates = { - ...uploadStates, - [info.file.name]: { state: State.Valid, infos: [] }, - }; - } catch (error) { - uploadStates = { - ...uploadStates, - [info.file.name]: { state: State.Error, infos: [] }, - }; - exception = true; - } - - setProcessState(uploadStates); - } - } - - if (exception) { - throw new Error("Error when uploading subtitles"); - } + BackgroundTask.dispatch(TaskGroupName, tasks); }, [payload, pending, language]); const canUpload = useMemo( @@ -232,47 +163,34 @@ const SeriesUploadModal: FunctionComponent = ({ [pending, language] ); - const tableShow = pending.length > 0; + const showTable = pending.length > 0; const columns = useMemo[]>( () => [ { id: "Icon", - accessor: "instance", + accessor: "state", className: "text-center", - Cell: ({ row, loose }) => { - const { file } = row.original; - - const name = file.name; - const states = loose![1] as ProcessState; - + Cell: ({ value: state }) => { let icon = faCircleNotch; let color: string | undefined = undefined; let spin = false; let msgs: string[] = []; - if (name in states) { - const state = states[name]; - msgs = state.infos; - switch (state.state) { - case State.Error: - icon = faExclamationTriangle; - color = "var(--danger)"; - break; - case State.Valid: - icon = faCheck; - color = "var(--success)"; - break; - case State.Warning: - icon = faInfoCircle; - color = "var(--warning)"; - break; - case State.Update: - spin = true; - break; - default: - break; - } + switch (state) { + case State.Valid: + icon = faCheck; + color = "var(--success)"; + break; + case State.Warning: + icon = faInfoCircle; + color = "var(--warning)"; + break; + case State.Updating: + spin = true; + break; + default: + break; } return ( @@ -295,7 +213,7 @@ const SeriesUploadModal: FunctionComponent = ({ className: "vw-1", Cell: ({ value, loose, row, externalUpdate }) => { const uploading = loose![0] as boolean; - const availables = loose![2] as Item.Episode[]; + const availables = loose![1] as Item.Episode[]; const options = availables.map>( (ep) => ({ @@ -414,18 +332,18 @@ const SeriesUploadModal: FunctionComponent = ({ -