mirror of
https://github.com/morpheus65535/bazarr
synced 2025-02-21 21:47:15 +00:00
Upload serie subtitles in background
This commit is contained in:
parent
1e50c515d8
commit
43ebecbdb2
6 changed files with 87 additions and 144 deletions
|
@ -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) {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>>(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue