Upload serie subtitles in background

pull/1515/head
LASER-Yi 3 years ago
parent 1e50c515d8
commit 43ebecbdb2

@ -11,7 +11,7 @@ import React, { FunctionComponent, useState } from "react";
import { Alert, Container, Row } from "react-bootstrap"; import { Alert, Container, Row } from "react-bootstrap";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Redirect, RouteComponentProps, withRouter } from "react-router-dom"; 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 { useMovieBy, useProfileBy } from "../../@redux/hooks";
import { MoviesApi, ProvidersApi } from "../../apis"; import { MoviesApi, ProvidersApi } from "../../apis";
import { import {
@ -24,7 +24,6 @@ import {
useShowModal, useShowModal,
} from "../../components"; } from "../../components";
import { ManualSearchModal } from "../../components/modals/ManualSearchModal"; import { ManualSearchModal } from "../../components/modals/ManualSearchModal";
import { TaskGroupName } from "../../components/modals/MovieUploadModal";
import ItemOverview from "../../generic/ItemOverview"; import ItemOverview from "../../generic/ItemOverview";
import { RouterEmptyPath } from "../../special-pages/404"; import { RouterEmptyPath } from "../../special-pages/404";
import { useOnLoadedOnce } from "../../utilites"; import { useOnLoadedOnce } from "../../utilites";
@ -59,7 +58,7 @@ const MovieDetailView: FunctionComponent<Props> = ({ match }) => {
const [valid, setValid] = useState(true); const [valid, setValid] = useState(true);
const hasTask = useIsGroupTaskRunningWithId(TaskGroupName, id); const hasTask = useIsAnyTaskRunningWithId(id);
useOnLoadedOnce(() => { useOnLoadedOnce(() => {
if (movie.content === null) { if (movie.content === null) {

@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useMemo } from "react"; import React, { FunctionComponent, useMemo } from "react";
import { Badge } from "react-bootstrap"; import { Badge } from "react-bootstrap";
import { Column } from "react-table"; import { Column } from "react-table";
import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useProfileItemsToLanguages } from "../../@redux/hooks"; import { useProfileItemsToLanguages } from "../../@redux/hooks";
import { useShowOnlyDesired } from "../../@redux/hooks/site"; import { useShowOnlyDesired } from "../../@redux/hooks/site";
import { MoviesApi } from "../../apis"; import { MoviesApi } from "../../apis";
@ -21,6 +22,8 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
const profileItems = useProfileItemsToLanguages(profile); const profileItems = useProfileItemsToLanguages(profile);
const hasTask = useIsAnyTaskRunningWithId(movie.radarrId);
const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>( const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>(
() => [ () => [
{ {
@ -64,6 +67,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else if (original.path === missingText) { } else if (original.path === missingText) {
return ( return (
<AsyncButton <AsyncButton
disabled={hasTask}
promise={() => promise={() =>
MoviesApi.downloadSubtitles(movie.radarrId, { MoviesApi.downloadSubtitles(movie.radarrId, {
language: original.code2, language: original.code2,
@ -80,6 +84,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else { } else {
return ( return (
<AsyncButton <AsyncButton
disabled={hasTask}
variant="light" variant="light"
size="sm" size="sm"
promise={() => promise={() =>
@ -98,7 +103,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
}, },
}, },
], ],
[movie] [movie, hasTask]
); );
const data: Subtitle[] = useMemo(() => { const data: Subtitle[] = useMemo(() => {

@ -8,9 +8,10 @@ import {
faWrench, faWrench,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import React, { FunctionComponent, useMemo, useState } from "react"; 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 { Helmet } from "react-helmet";
import { Redirect, RouteComponentProps, withRouter } from "react-router-dom"; import { Redirect, RouteComponentProps, withRouter } from "react-router-dom";
import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useEpisodesBy, useProfileBy, useSerieBy } from "../../@redux/hooks"; import { useEpisodesBy, useProfileBy, useSerieBy } from "../../@redux/hooks";
import { SeriesApi } from "../../apis"; import { SeriesApi } from "../../apis";
import { import {
@ -66,6 +67,8 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
const profile = useProfileBy(series.content?.profileId); const profile = useProfileBy(series.content?.profileId);
const hasTask = useIsAnyTaskRunningWithId(id);
if (isNaN(id) || !valid) { if (isNaN(id) || !valid) {
return <Redirect to={RouterEmptyPath}></Redirect>; return <Redirect to={RouterEmptyPath}></Redirect>;
} }
@ -83,7 +86,7 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
<ContentHeader.Group pos="start"> <ContentHeader.Group pos="start">
<ContentHeader.AsyncButton <ContentHeader.AsyncButton
icon={faSync} icon={faSync}
disabled={!available} disabled={!available || hasTask}
promise={() => promise={() =>
SeriesApi.action({ action: "scan-disk", seriesid: id }) SeriesApi.action({ action: "scan-disk", seriesid: id })
} }
@ -98,7 +101,8 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
disabled={ disabled={
serie.episodeFileCount === 0 || serie.episodeFileCount === 0 ||
serie.profileId === null || serie.profileId === null ||
!available !available ||
hasTask
} }
> >
Search Search
@ -106,7 +110,7 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
</ContentHeader.Group> </ContentHeader.Group>
<ContentHeader.Group pos="end"> <ContentHeader.Group pos="end">
<ContentHeader.Button <ContentHeader.Button
disabled={serie.episodeFileCount === 0 || !available} disabled={serie.episodeFileCount === 0 || !available || hasTask}
icon={faBriefcase} icon={faBriefcase}
onClick={() => showModal("tools", episodes.content)} onClick={() => showModal("tools", episodes.content)}
> >
@ -125,12 +129,23 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
</ContentHeader.Button> </ContentHeader.Button>
<ContentHeader.Button <ContentHeader.Button
icon={faWrench} icon={faWrench}
disabled={hasTask}
onClick={() => showModal("edit", serie)} onClick={() => showModal("edit", serie)}
> >
Edit Series Edit Series
</ContentHeader.Button> </ContentHeader.Button>
</ContentHeader.Group> </ContentHeader.Group>
</ContentHeader> </ContentHeader>
<Row>
<Alert
className="w-100 m-0 py-2"
show={hasTask}
style={{ borderRadius: 0 }}
variant="light"
>
A background task is running for this show, actions are unavailable
</Alert>
</Row>
<Row> <Row>
<ItemOverview item={serie} details={details}></ItemOverview> <ItemOverview item={serie} details={details}></ItemOverview>
</Row> </Row>

@ -9,6 +9,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useCallback, useMemo } from "react"; import React, { FunctionComponent, useCallback, useMemo } from "react";
import { Badge, ButtonGroup } from "react-bootstrap"; import { Badge, ButtonGroup } from "react-bootstrap";
import { Column, TableUpdater } from "react-table"; import { Column, TableUpdater } from "react-table";
import { useIsAnyTaskRunningWithId } from "../../@modules/task/hooks";
import { useProfileItemsToLanguages } from "../../@redux/hooks"; import { useProfileItemsToLanguages } from "../../@redux/hooks";
import { useShowOnlyDesired } from "../../@redux/hooks/site"; import { useShowOnlyDesired } from "../../@redux/hooks/site";
import { ProvidersApi } from "../../apis"; import { ProvidersApi } from "../../apis";
@ -54,6 +55,10 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
const profileItems = useProfileItemsToLanguages(profile); const profileItems = useProfileItemsToLanguages(profile);
const hasTask = useIsAnyTaskRunningWithId(
serie.content?.sonarrSeriesId ?? -1
);
const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>( const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>(
() => [ () => [
{ {
@ -101,7 +106,7 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
{ {
Header: "Subtitles", Header: "Subtitles",
accessor: "missing_subtitles", accessor: "missing_subtitles",
Cell: ({ row, loose }) => { Cell: ({ row }) => {
const episode = row.original; const episode = row.original;
const seriesid = episode.sonarrSeriesId; const seriesid = episode.sonarrSeriesId;
@ -147,7 +152,7 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
<ButtonGroup> <ButtonGroup>
<ActionButton <ActionButton
icon={faUser} icon={faUser}
disabled={serie.content?.profileId === null} disabled={serie.content?.profileId === null || hasTask}
onClick={() => { onClick={() => {
externalUpdate && externalUpdate(row, "manual-search"); externalUpdate && externalUpdate(row, "manual-search");
}} }}
@ -160,6 +165,7 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
></ActionButton> ></ActionButton>
<ActionButton <ActionButton
icon={faBriefcase} icon={faBriefcase}
disabled={hasTask}
onClick={() => { onClick={() => {
externalUpdate && externalUpdate(row, "tools"); externalUpdate && externalUpdate(row, "tools");
}} }}
@ -169,7 +175,7 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
}, },
}, },
], ],
[onlyDesired, profileItems, serie] [onlyDesired, profileItems, serie, hasTask]
); );
const updateRow = useCallback<TableUpdater<Item.Episode>>( const updateRow = useCallback<TableUpdater<Item.Episode>>(

@ -12,7 +12,7 @@ import { MoviesApi } from "../../apis";
import BaseModal, { BaseModalProps } from "./BaseModal"; import BaseModal, { BaseModalProps } from "./BaseModal";
import { useModalInformation } from "./hooks"; import { useModalInformation } from "./hooks";
export const TaskGroupName = "Uploading Movie Subtitles..."; export const TaskGroupName = "Uploading Subtitles...";
const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => { const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
const modal = props; const modal = props;

@ -1,7 +1,6 @@
import { import {
faCheck, faCheck,
faCircleNotch, faCircleNotch,
faExclamationTriangle,
faInfoCircle, faInfoCircle,
faTrash, faTrash,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
@ -22,6 +21,8 @@ import {
MessageIcon, MessageIcon,
SimpleTable, SimpleTable,
} from ".."; } from "..";
import BackgroundTask from "../../@modules/task";
import { createTask } from "../../@modules/task/utilites";
import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks"; import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks";
import { EpisodesApi, SubtitlesApi } from "../../apis"; import { EpisodesApi, SubtitlesApi } from "../../apis";
import { Selector } from "../inputs"; import { Selector } from "../inputs";
@ -29,27 +30,17 @@ import BaseModal, { BaseModalProps } from "./BaseModal";
import { useModalInformation } from "./hooks"; import { useModalInformation } from "./hooks";
enum State { enum State {
Update, Updating,
Valid, Valid,
Warning, Warning,
Error,
} }
interface PendingSubtitle { interface PendingSubtitle {
file: File; file: File;
didCheck: boolean; state: State;
instance?: Item.Episode; instance?: Item.Episode;
} }
type SubtitleState = {
state: State;
infos: string[];
};
type ProcessState = {
[name: string]: SubtitleState;
};
type EpisodeMap = { type EpisodeMap = {
[name: string]: Item.Episode; [name: string]: Item.Episode;
}; };
@ -58,6 +49,8 @@ interface SerieProps {
episodes: readonly Item.Episode[]; episodes: readonly Item.Episode[];
} }
export const TaskGroupName = "Uploading Subtitles...";
const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
episodes, episodes,
...modal ...modal
@ -70,8 +63,6 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
const [pending, setPending] = useState<PendingSubtitle[]>([]); const [pending, setPending] = useState<PendingSubtitle[]>([]);
const [processState, setProcessState] = useState<ProcessState>({});
const profile = useProfileBy(payload?.profileId); const profile = useProfileBy(payload?.profileId);
const avaliableLanguages = useProfileItemsToLanguages(profile); const avaliableLanguages = useProfileItemsToLanguages(profile);
@ -86,38 +77,6 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
const filelist = useMemo(() => pending.map((v) => v.file), [pending]); const filelist = useMemo(() => pending.map((v) => v.file), [pending]);
// Vaildate
useEffect(() => {
const states = pending.reduce<ProcessState>((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( const checkEpisodes = useCallback(
async (list: PendingSubtitle[]) => { async (list: PendingSubtitle[]) => {
const names = list.map((v) => v.file.name); const names = list.map((v) => v.file.name);
@ -138,7 +97,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
setPending((pd) => setPending((pd) =>
pd.map((v) => ({ pd.map((v) => ({
...v, ...v,
didCheck: true, state: State.Valid,
instance: episodeMap[v.file.name], instance: episodeMap[v.file.name],
})) }))
); );
@ -154,18 +113,10 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return { return {
file: f, file: f,
didCheck: false, didCheck: false,
state: State.Updating,
}; };
}); });
setPending(list); setPending(list);
const states = files.reduce<ProcessState>(
(v, curr) => ({
...v,
[curr.name]: { state: State.Update, infos: [] },
}),
{}
);
setProcessState(states);
checkEpisodes(list); checkEpisodes(list);
}, },
[checkEpisodes] [checkEpisodes]
@ -177,51 +128,31 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
} }
const { sonarrSeriesId: seriesid } = payload; const { sonarrSeriesId: seriesid } = payload;
let uploadStates = pending.reduce<ProcessState>((prev, curr) => {
prev[curr.file.name] = { state: State.Update, infos: [] };
return prev;
}, {});
setProcessState(uploadStates);
let exception = false;
for (const info of pending) {
if (info.instance) {
const { sonarrEpisodeId: episodeid } = info.instance;
const { file } = info;
const { code2, hi, forced } = language; const { code2, hi, forced } = language;
try { const tasks = pending
.filter((v) => v.instance !== undefined)
.map((v) => {
const { sonarrEpisodeId: episodeid } = v.instance!;
const form: FormType.UploadSubtitle = { const form: FormType.UploadSubtitle = {
file, file: v.file,
language: code2, language: code2,
hi: hi ?? false, hi: hi ?? false,
forced: forced ?? false, forced: forced ?? false,
}; };
await EpisodesApi.uploadSubtitles(seriesid, episodeid, form); return createTask(
v.file.name,
uploadStates = { seriesid,
...uploadStates, EpisodesApi.uploadSubtitles.bind(EpisodesApi),
[info.file.name]: { state: State.Valid, infos: [] }, seriesid,
}; episodeid,
} catch (error) { form
uploadStates = { );
...uploadStates, });
[info.file.name]: { state: State.Error, infos: [] },
};
exception = true;
}
setProcessState(uploadStates);
}
}
if (exception) { BackgroundTask.dispatch(TaskGroupName, tasks);
throw new Error("Error when uploading subtitles");
}
}, [payload, pending, language]); }, [payload, pending, language]);
const canUpload = useMemo( const canUpload = useMemo(
@ -232,33 +163,21 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
[pending, language] [pending, language]
); );
const tableShow = pending.length > 0; const showTable = pending.length > 0;
const columns = useMemo<Column<PendingSubtitle>[]>( const columns = useMemo<Column<PendingSubtitle>[]>(
() => [ () => [
{ {
id: "Icon", id: "Icon",
accessor: "instance", accessor: "state",
className: "text-center", className: "text-center",
Cell: ({ row, loose }) => { Cell: ({ value: state }) => {
const { file } = row.original;
const name = file.name;
const states = loose![1] as ProcessState;
let icon = faCircleNotch; let icon = faCircleNotch;
let color: string | undefined = undefined; let color: string | undefined = undefined;
let spin = false; let spin = false;
let msgs: string[] = []; let msgs: string[] = [];
if (name in states) { switch (state) {
const state = states[name];
msgs = state.infos;
switch (state.state) {
case State.Error:
icon = faExclamationTriangle;
color = "var(--danger)";
break;
case State.Valid: case State.Valid:
icon = faCheck; icon = faCheck;
color = "var(--success)"; color = "var(--success)";
@ -267,13 +186,12 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
icon = faInfoCircle; icon = faInfoCircle;
color = "var(--warning)"; color = "var(--warning)";
break; break;
case State.Update: case State.Updating:
spin = true; spin = true;
break; break;
default: default:
break; break;
} }
}
return ( return (
<MessageIcon <MessageIcon
@ -295,7 +213,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
className: "vw-1", className: "vw-1",
Cell: ({ value, loose, row, externalUpdate }) => { Cell: ({ value, loose, row, externalUpdate }) => {
const uploading = loose![0] as boolean; const uploading = loose![0] as boolean;
const availables = loose![2] as Item.Episode[]; const availables = loose![1] as Item.Episode[];
const options = availables.map<SelectorOption<Item.Episode>>( const options = availables.map<SelectorOption<Item.Episode>>(
(ep) => ({ (ep) => ({
@ -414,18 +332,18 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
<Form.Group> <Form.Group>
<FileForm <FileForm
emptyText="Select..." emptyText="Select..."
disabled={tableShow || avaliableLanguages.length === 0} disabled={showTable || avaliableLanguages.length === 0}
multiple multiple
value={filelist} value={filelist}
onChange={setFiles} onChange={setFiles}
></FileForm> ></FileForm>
</Form.Group> </Form.Group>
</Form> </Form>
<div hidden={!tableShow}> <div hidden={!showTable}>
<SimpleTable <SimpleTable
columns={columns} columns={columns}
data={pending} data={pending}
loose={[uploading, processState, episodes]} loose={[uploading, episodes]}
responsive={false} responsive={false}
externalUpdate={updateItem} externalUpdate={updateItem}
></SimpleTable> ></SimpleTable>

Loading…
Cancel
Save