Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend) - Export to Podlove Publisher #394

Merged
merged 4 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 129 additions & 23 deletions frontend/src/editor/export/webvtt.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import * as Automerge from '@automerge/automerge';

import { Checkbox, FormControl, Input, Select } from '../../components/form';
import { canGenerateVtt, generateWebVtt } from '../../utils/export/webvtt';
import { SubtitleFormat } from '@audapolis/webvtt-writer';
import { downloadTextAsFile } from '../../utils/download_text_as_file';
import { pushToPodlove, checkIsPodloveExportPossible } from '../../utils/export_to_podlove';
import { ExportProps } from '.';
import { PrimaryButton, SecondaryButton } from '../../components/button';
import { PrimaryButton, SecondaryButton, IconButton } from '../../components/button';
import { BsEye, BsEyeSlash } from 'react-icons/bs';

type ExportFormat = SubtitleFormat | 'podlove';

export function WebVttExportBody({ onClose, outputNameBase, editor }: ExportProps) {
const [includeSpeakerNames, setIncludeSpeakerNames] = useState(true);
const [includeWordTimings, setIncludeWordTimings] = useState(false);
const [limitLineLength, setLimitLineLength] = useState(false);
const [maxLineLength, setMaxLineLength] = useState(60);
const [format, setFormat] = useState('vtt' as SubtitleFormat);

const [podloveEpisodeId, setPodloveEpisodeId] = useState(1);
const [podloveUser, setPodloveUser] = useState('');
const [podloveShowApplicationId, setPodloveShowApplicationId] = useState(false);
const [podloveApplicationId, setPodloveId] = useState('');
const [podloveUrl, setPodloveUrl] = useState('');
const [podloveExportPossible, setPodloveExportPossible] = useState(false);
useEffect(() => {
checkIsPodloveExportPossible(
podloveEpisodeId,
podloveUser,
podloveApplicationId,
podloveUrl,
).then(setPodloveExportPossible);
}, [podloveEpisodeId, podloveUser, podloveApplicationId, podloveApplicationId, podloveUrl]);

const [format, setFormat] = useState('vtt' as ExportFormat);
const canExport = useMemo(() => canGenerateVtt(editor.doc.children), [editor.v]);

return (
Expand All @@ -22,31 +42,100 @@ export function WebVttExportBody({ onClose, outputNameBase, editor }: ExportProp
<Select
value={format}
onChange={(e) => {
if (e.target.value === 'srt' || e.target.value === 'vtt') {
if (
e.target.value === 'srt' ||
e.target.value === 'vtt' ||
e.target.value === 'podlove'
) {
setFormat(e.target.value);
}
}}
>
<option value="vtt">WebVTT</option>
<option value="srt">SRT</option>
<option value="podlove">Upload to Podlove Publisher</option>
</Select>
</FormControl>
{format == 'vtt' ? (
{format == 'podlove' ? (
<>
<Checkbox
label="Include Speaker Names"
value={format == 'vtt' && includeSpeakerNames}
onChange={(x) => setIncludeSpeakerNames(x)}
/>
<Checkbox
label="Include Word-Timings"
value={format == 'vtt' && includeWordTimings}
onChange={(x) => setIncludeWordTimings(x)}
/>
<FormControl label={'Podlove Publisher baseUrl'}>
<Input
autoFocus
value={podloveUrl}
type="string"
onChange={(e) => {
setPodloveUrl(e.target.value);
}}
/>
</FormControl>
<FormControl label={'User'}>
<Input
autoFocus
value={podloveUser}
type="string"
onChange={(e) => {
setPodloveUser(e.target.value);
}}
/>
</FormControl>
<FormControl label={'Application Password'}>
<div className="flex">
<Input
autoFocus
value={podloveApplicationId}
type={podloveShowApplicationId ? 'text' : 'password'}
onChange={(e) => {
setPodloveId(e.target.value);
}}
/>
<IconButton
icon={podloveShowApplicationId ? BsEyeSlash : BsEye}
size={20}
onClick={(e) => {
e.preventDefault();
setPodloveShowApplicationId(!podloveShowApplicationId);
}}
label={podloveShowApplicationId ? 'Hide' : 'Show'}
iconClassName="inline-block -mt-1"
className="rounded-xl px-4 py-1"
iconAfter={true}
></IconButton>
</div>
</FormControl>
<FormControl label={'Episode (id)'}>
<Input
autoFocus
value={podloveEpisodeId}
type="number"
onChange={(e) => {
setPodloveEpisodeId(parseInt(e.target.value));
}}
/>
</FormControl>
</>
) : (
<></>
)}
{format == 'vtt' || format == 'podlove' ? (
<Checkbox
label="Include Speaker Names"
value={(format == 'vtt' || format == 'podlove') && includeSpeakerNames}
onChange={(x) => setIncludeSpeakerNames(x)}
/>
) : (
<></>
)}
{format == 'vtt' ? (
<Checkbox
label="Include Word-Timings"
value={format == 'vtt' && includeWordTimings}
onChange={(x) => {
setIncludeWordTimings(x);
}}
/>
) : (
<></>
)}
<Checkbox
label="Limit line length"
value={limitLineLength}
Expand All @@ -63,9 +152,16 @@ export function WebVttExportBody({ onClose, outputNameBase, editor }: ExportProp
disabled={!limitLineLength}
/>
</FormControl>
{!canExport.canGenerate && canExport.reason && (
{((!canExport.canGenerate && canExport.reason) ||
(format === 'podlove' && !podloveExportPossible)) && (
<div className="block bg-red-100 px-2 py-2 rounded text-center text-red-700">
{canExport.reason}
<p>{canExport.reason}</p>
{format === 'podlove' && !podloveExportPossible && (
<p>
Configured episode could not be found in the podlove publisher instance. Please check
that the publisher url, credentials and episode id are correct.
</p>
)}
</div>
)}
<div className="flex justify-between">
Expand All @@ -82,14 +178,24 @@ export function WebVttExportBody({ onClose, outputNameBase, editor }: ExportProp
includeWordTimings,
maxLineLength,
);
downloadTextAsFile(
`${outputNameBase}.${format}`,
`text/${format}`,
vtt.toString(format),
);
if (format === 'vtt' || format === 'srt') {
downloadTextAsFile(
`${outputNameBase}.${format}`,
`text/${format}`,
vtt.toString(format),
);
} else {
pushToPodlove(
podloveEpisodeId,
podloveUser,
podloveApplicationId,
podloveUrl,
vtt.toString('vtt'),
);
}
onClose();
}}
disabled={!canExport.canGenerate}
disabled={!canExport.canGenerate || (!podloveExportPossible && format == 'podlove')}
>
Export
</PrimaryButton>
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/utils/export_to_podlove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export function pushToPodlove(
episodeId: number,
user: string,
appId: string,
url: string,
text: string,
) {
// check if episode exist
const podloveUrlEpispde = url + '/wp-json/podlove/v2/episodes/' + episodeId.toString();
fetch(podloveUrlEpispde, {
method: 'GET',
headers: {
'Content-type': 'application/json;charset=UTF-8',
Authorization: `Basic ${btoa(`${user}:${appId}`)}`,
},
})
.then((response) => {
// export the vtt to the podlove publisher
if (response.status === 200) {
const podloveUrlTranscript =
url + '/wp-json/podlove/v2/transcripts/' + episodeId.toString();
const podloveData = {
type: 'vtt',
content: text,
};
fetch(podloveUrlTranscript, {
method: 'POST',
body: JSON.stringify(podloveData),
headers: {
'Content-type': 'application/json;charset=UTF-8',
Authorization: `Basic ${btoa(`${user}:${appId}`)}`,
},
})
.then((response) => response.json())
.catch((err) => console.error(err));
}
})
.catch((err) => console.error(err));
}

export async function checkIsPodloveExportPossible(
episodeId: number,
user: string,
appId: string,
url: string,
): Promise<boolean> {
if (url.length < 1 || appId.length < 1 || user.length < 1 || episodeId < 1) {
return false;
}

const podloveUrlEpisode = url + '/wp-json/podlove/v2/episodes/' + episodeId.toString();
try {
const response = await fetch(podloveUrlEpisode, {
method: 'GET',
headers: {
'Content-type': 'application/json;charset=UTF-8',
Authorization: `Basic ${btoa(`${user}:${appId}`)}`,
},
});
if (response.status === 200) {
return true;
} else {
return false;
}
} catch (err) {
console.log(err);
return false;
}
}