Upload serie subtitles in background

This commit is contained in:
LASER-Yi 2021-08-22 12:30:03 +08:00
parent 1e50c515d8
commit 43ebecbdb2
6 changed files with 87 additions and 144 deletions

View File

@ -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<Props> = ({ match }) => {
const [valid, setValid] = useState(true);
const hasTask = useIsGroupTaskRunningWithId(TaskGroupName, id);
const hasTask = useIsAnyTaskRunningWithId(id);
useOnLoadedOnce(() => {
if (movie.content === null) {

View File

@ -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<Props> = ({ movie, profile }) => {
const profileItems = useProfileItemsToLanguages(profile);
const hasTask = useIsAnyTaskRunningWithId(movie.radarrId);
const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>(
() => [
{
@ -64,6 +67,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else if (original.path === missingText) {
return (
<AsyncButton
disabled={hasTask}
promise={() =>
MoviesApi.downloadSubtitles(movie.radarrId, {
language: original.code2,
@ -80,6 +84,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
} else {
return (
<AsyncButton
disabled={hasTask}
variant="light"
size="sm"
promise={() =>
@ -98,7 +103,7 @@ const Table: FunctionComponent<Props> = ({ movie, profile }) => {
},
},
],
[movie]
[movie, hasTask]
);
const data: Subtitle[] = useMemo(() => {

View File

@ -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> = (props) => {
const profile = useProfileBy(series.content?.profileId);
const hasTask = useIsAnyTaskRunningWithId(id);
if (isNaN(id) || !valid) {
return <Redirect to={RouterEmptyPath}></Redirect>;
}
@ -83,7 +86,7 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
<ContentHeader.Group pos="start">
<ContentHeader.AsyncButton
icon={faSync}
disabled={!available}
disabled={!available || hasTask}
promise={() =>
SeriesApi.action({ action: "scan-disk", seriesid: id })
}
@ -98,7 +101,8 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
disabled={
serie.episodeFileCount === 0 ||
serie.profileId === null ||
!available
!available ||
hasTask
}
>
Search
@ -106,7 +110,7 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
</ContentHeader.Group>
<ContentHeader.Group pos="end">
<ContentHeader.Button
disabled={serie.episodeFileCount === 0 || !available}
disabled={serie.episodeFileCount === 0 || !available || hasTask}
icon={faBriefcase}
onClick={() => showModal("tools", episodes.content)}
>
@ -125,12 +129,23 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
</ContentHeader.Button>
<ContentHeader.Button
icon={faWrench}
disabled={hasTask}
onClick={() => showModal("edit", serie)}
>
Edit Series
</ContentHeader.Button>
</ContentHeader.Group>
</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>
<ItemOverview item={serie} details={details}></ItemOverview>
</Row>

View File

@ -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<Props> = ({ serie, episodes, profile }) => {
const profileItems = useProfileItemsToLanguages(profile);
const hasTask = useIsAnyTaskRunningWithId(
serie.content?.sonarrSeriesId ?? -1
);
const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>(
() => [
{
@ -101,7 +106,7 @@ const Table: FunctionComponent<Props> = ({ 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<Props> = ({ serie, episodes, profile }) => {
<ButtonGroup>
<ActionButton
icon={faUser}
disabled={serie.content?.profileId === null}
disabled={serie.content?.profileId === null || hasTask}
onClick={() => {
externalUpdate && externalUpdate(row, "manual-search");
}}
@ -160,6 +165,7 @@ const Table: FunctionComponent<Props> = ({ serie, episodes, profile }) => {
></ActionButton>
<ActionButton
icon={faBriefcase}
disabled={hasTask}
onClick={() => {
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>>(

View File

@ -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<BaseModalProps> = (props) => {
const modal = props;

View File

@ -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<SerieProps & BaseModalProps> = ({
episodes,
...modal
@ -70,8 +63,6 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
const [pending, setPending] = useState<PendingSubtitle[]>([]);
const [processState, setProcessState] = useState<ProcessState>({});
const profile = useProfileBy(payload?.profileId);
const avaliableLanguages = useProfileItemsToLanguages(profile);
@ -86,38 +77,6 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
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(
async (list: PendingSubtitle[]) => {
const names = list.map((v) => v.file.name);
@ -138,7 +97,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
setPending((pd) =>
pd.map((v) => ({
...v,
didCheck: true,
state: State.Valid,
instance: episodeMap[v.file.name],
}))
);
@ -154,18 +113,10 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return {
file: f,
didCheck: false,
state: State.Updating,
};
});
setPending(list);
const states = files.reduce<ProcessState>(
(v, curr) => ({
...v,
[curr.name]: { state: State.Update, infos: [] },
}),
{}
);
setProcessState(states);
checkEpisodes(list);
},
[checkEpisodes]
@ -177,51 +128,31 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
}
const { sonarrSeriesId: seriesid } = payload;
const { code2, hi, forced } = language;
let uploadStates = pending.reduce<ProcessState>((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<SerieProps & BaseModalProps> = ({
[pending, language]
);
const tableShow = pending.length > 0;
const showTable = pending.length > 0;
const columns = useMemo<Column<PendingSubtitle>[]>(
() => [
{
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<SerieProps & BaseModalProps> = ({
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<SelectorOption<Item.Episode>>(
(ep) => ({
@ -414,18 +332,18 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
<Form.Group>
<FileForm
emptyText="Select..."
disabled={tableShow || avaliableLanguages.length === 0}
disabled={showTable || avaliableLanguages.length === 0}
multiple
value={filelist}
onChange={setFiles}
></FileForm>
</Form.Group>
</Form>
<div hidden={!tableShow}>
<div hidden={!showTable}>
<SimpleTable
columns={columns}
data={pending}
loose={[uploading, processState, episodes]}
loose={[uploading, episodes]}
responsive={false}
externalUpdate={updateItem}
></SimpleTable>