mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
1277 lines
42 KiB
JavaScript
1277 lines
42 KiB
JavaScript
import React, {useState} from "react";
|
|
import {fetchVideoDownloaderConfig, getDownloaders, postDownload, putDownload} from "../api";
|
|
import {APIButton, DirectorySearch, InfoHeader, InfoPopup, mergeDeep, RequiredAsterisk, useLocalStorage} from "./Common";
|
|
import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
|
|
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
|
|
import {Button, Form, FormInput, Header} from "./Theme";
|
|
import {Form as SForm, FormDropdown} from "semantic-ui-react";
|
|
import {Link} from "react-router-dom";
|
|
import {TagsSelector} from "../Tags";
|
|
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
|
|
import {commaSeparatedValidator, InputForm, NumberInputForm, ToggleForm, UrlInput, UrlsTextarea, useForm} from "../hooks/useForm";
|
|
import {
|
|
channelFrequencyOptions,
|
|
days30Option,
|
|
defaultVideoFormatOption,
|
|
defaultVideoResolutionOptions,
|
|
Downloaders,
|
|
downloadFormatOptions,
|
|
downloadOrderOptions,
|
|
downloadResolutionOptions,
|
|
extendedFrequencyOptions,
|
|
frequencyOptions,
|
|
weeklyOption
|
|
} from "./Vars";
|
|
|
|
export function DepthInputForm({form, required, name = 'depth', path = 'settings.depth'}) {
|
|
return <NumberInputForm
|
|
form={form}
|
|
required={required}
|
|
name={name}
|
|
path={path}
|
|
label='Depth'
|
|
max={4}
|
|
helpContent='Search the URLs provided, and any URLs they contain up to this depth. Warning: This can be exponential!'
|
|
/>
|
|
}
|
|
|
|
export function MaximumPagesInputForm({form, required, name = 'max_pages', path = 'settings.max_pages'}) {
|
|
return <NumberInputForm
|
|
form={form}
|
|
required={required}
|
|
name={name}
|
|
path={path}
|
|
label='Maximum Pages'
|
|
max={100000}
|
|
helpContent='Stop searching for files if this many pages have been searched.'
|
|
/>
|
|
}
|
|
|
|
export function DestinationForm({
|
|
form,
|
|
infoContent,
|
|
label = 'Destination',
|
|
name = 'destination',
|
|
path = 'destination',
|
|
required = false,
|
|
}) {
|
|
const [inputProps, inputAttrs] = form.getCustomProps({name, path, required});
|
|
const {disabled, value, onChange} = inputProps;
|
|
const infoPopup = infoContent ? <InfoPopup content={infoContent}/> : null;
|
|
return <SForm.Field>
|
|
<label>{label} {required && <RequiredAsterisk/>}{infoPopup}</label>
|
|
<DirectorySearch
|
|
required={required}
|
|
value={value}
|
|
onSelect={onChange}
|
|
disabled={disabled}
|
|
id='destination_search_form'
|
|
/>
|
|
</SForm.Field>
|
|
}
|
|
|
|
export function DownloadTagsSelector({form, limit, path = 'tag_names', name = 'tag_names'}) {
|
|
const [inputProps, inputAttrs] = form.getCustomProps({name, path});
|
|
const {value, onChange} = inputProps;
|
|
|
|
return <TagsSelector
|
|
disabled={form.disabled}
|
|
selectedTagNames={value}
|
|
limit={limit}
|
|
onChange={onChange}
|
|
closeAfterAdd={!!limit}
|
|
/>;
|
|
}
|
|
|
|
export function DownloadFrequencySelector({
|
|
form,
|
|
freqOptions = frequencyOptions,
|
|
name = 'frequency',
|
|
path = 'frequency',
|
|
}) {
|
|
const [inputProps, inputAttrs] = form.getSelectionProps({name: 'frequency', path});
|
|
|
|
return <FormDropdown
|
|
required
|
|
selection
|
|
label='Download Frequency'
|
|
placeholder='Frequency'
|
|
options={freqOptions}
|
|
name={name}
|
|
id='download_frequency_selector'
|
|
{...inputProps}
|
|
/>
|
|
}
|
|
|
|
export function DownloaderSelector({form, name = 'sub_downloader', path = 'sub_downloader'}) {
|
|
const [downloaders, setDownloaders] = React.useState([]);
|
|
const [inputProps, inputAttrs] = form.getSelectionProps({name, path, required: true});
|
|
|
|
const fetchDownloaders = async () => {
|
|
let {downloaders: downloaders_} = await getDownloaders();
|
|
downloaders_ = downloaders_.map((i) => {
|
|
return {key: i.name, text: i.pretty_name || i.name, value: i.name}
|
|
})
|
|
setDownloaders(downloaders_);
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
fetchDownloaders();
|
|
}, []);
|
|
|
|
return <FormDropdown selection required
|
|
label='Downloader'
|
|
placeholder='Select a downloader'
|
|
options={downloaders}
|
|
id='downloader_selector'
|
|
{...inputProps}
|
|
/>
|
|
}
|
|
|
|
export function ExcludedUrls({form, name = 'excluded_urls', path = 'settings.excluded_urls'}) {
|
|
return <InputForm
|
|
form={form}
|
|
type='text'
|
|
name={name}
|
|
path={path}
|
|
helpContent="Comma-separated list of keywords that will be ignored if they are in any link's URL"
|
|
label='Excluded URLs'
|
|
placeholder='prize,gift'
|
|
validator={commaSeparatedValidator}
|
|
/>
|
|
}
|
|
|
|
export function TitleInclusionInput({form, path = 'settings.title_include'}) {
|
|
const [inputProps, inputAttrs] = form.getInputProps({
|
|
name: 'title_include',
|
|
path,
|
|
validator: commaSeparatedValidator,
|
|
});
|
|
|
|
return <>
|
|
<InfoHeader
|
|
headerSize='h4'
|
|
headerContent='Title Match Words'
|
|
popupContent='List of words, separated by commas, that titles must contain to be downloaded.'
|
|
/>
|
|
<FormInput
|
|
placeholder='Shelter,Solar Power'
|
|
error={inputProps.error}
|
|
>
|
|
<input {...inputProps}/>
|
|
</FormInput>
|
|
</>
|
|
}
|
|
|
|
export function TitleExclusionInput({form, path = 'settings.title_exclude'}) {
|
|
const [inputProps, inputAttrs] = form.getInputProps({
|
|
name: 'title_exclude',
|
|
path,
|
|
validator: commaSeparatedValidator,
|
|
});
|
|
|
|
return <>
|
|
<InfoHeader
|
|
headerSize='h4'
|
|
headerContent='Title Exclusion Words'
|
|
popupContent='List of words, separated by commas, that may not appear in titles to be downloaded.'
|
|
/>
|
|
<FormInput
|
|
placeholder='Giveaway,Prize'
|
|
error={inputProps.error}
|
|
>
|
|
<input {...inputProps}/>
|
|
</FormInput>
|
|
</>
|
|
}
|
|
|
|
export function DownloadFormButtons({onCancel, form}) {
|
|
return <Grid columns={2}>
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='left'>
|
|
<Button content='Cancel' onClick={onCancel} type='button'/>
|
|
</Grid.Column>
|
|
<Grid.Column textAlign='right'>
|
|
<APIButton
|
|
disabled={form.disabled || !form.ready}
|
|
type='submit'
|
|
style={{marginTop: '0.5em'}}
|
|
onClick={form.onSubmit}
|
|
id='download_form_download_button'
|
|
>Download</APIButton>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
}
|
|
|
|
export function EditDownloadFormButtons({onDelete, onCancel, form}) {
|
|
return <Grid columns={2}>
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='left'>
|
|
<APIButton
|
|
confirmButton='Delete'
|
|
confirmContent='Delete this Download?'
|
|
confirmHeader='Delete'
|
|
color='red'
|
|
onClick={onDelete}
|
|
>Delete</APIButton>
|
|
</Grid.Column>
|
|
<Grid.Column textAlign='right'>
|
|
<Button content='Cancel' onClick={onCancel}/>
|
|
<APIButton
|
|
disabled={form.disabled || !form.ready}
|
|
type='submit'
|
|
style={{marginTop: '0.5em'}}
|
|
onClick={form.onSubmit}
|
|
>Save</APIButton>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
}
|
|
|
|
function VideoDownloadOrder({form, path = 'settings.download_order'}) {
|
|
const [inputProps, inputAttrs] = form.getSelectionProps({
|
|
name: 'download_order',
|
|
path,
|
|
});
|
|
|
|
return <FormDropdown selection
|
|
label='Download Order'
|
|
options={downloadOrderOptions}
|
|
{...inputProps}
|
|
/>
|
|
}
|
|
|
|
function VideoDownloadCountLimit({form, name = 'video_count_limit', path = 'settings.video_count_limit'}) {
|
|
return <NumberInputForm
|
|
form={form}
|
|
helpContent='Stop downloading videos from this channel/playlist when this many have been downloaded.'
|
|
helpPosition='top right'
|
|
name={name}
|
|
path={path}
|
|
label='Video Count Limit'
|
|
placeholder='100'
|
|
/>
|
|
}
|
|
|
|
export function VideoResolutionSelectorForm({form, name = 'video_resolutions', path = 'settings.video_resolutions'}) {
|
|
const [inputProps, inputAttrs] = form.getSelectionProps({name, path, type: 'array'});
|
|
|
|
return <>
|
|
<InfoHeader
|
|
headerSize='h5'
|
|
headerContent='Video Resolutions'
|
|
popupContent='Videos will be downloaded in the first available resolution from the list you select.'
|
|
for_='video_resolutions_input'
|
|
/>
|
|
<FormDropdown selection multiple
|
|
id='video_resolutions_input'
|
|
options={downloadResolutionOptions}
|
|
{...inputProps}
|
|
/>
|
|
</>
|
|
}
|
|
|
|
function VideoFormatSelectorForm({form, name = 'video_format', path = 'settings.video_format'}) {
|
|
const [inputProps, inputAttrs] = form.getSelectionProps({
|
|
name,
|
|
path,
|
|
defaultValue: defaultVideoFormatOption,
|
|
});
|
|
|
|
return <>
|
|
<InfoHeader
|
|
headerSize='h5'
|
|
headerContent='Video Format'
|
|
popupContent='Videos will be downloaded in this format, or transcoded if not available.'
|
|
for_='video_format_input'
|
|
/>
|
|
<FormDropdown selection
|
|
id='video_format_input'
|
|
options={downloadFormatOptions}
|
|
{...inputProps}
|
|
/>
|
|
</>
|
|
}
|
|
|
|
function VideoDurationLimit({form, name, path, label, helpContent, placeholder, helpPosition}) {
|
|
return <NumberInputForm
|
|
form={form}
|
|
helpContent={helpContent}
|
|
helpPosition={helpPosition}
|
|
placeholder={placeholder}
|
|
name={name}
|
|
path={path}
|
|
label={label}
|
|
/>
|
|
}
|
|
|
|
export function VideoTagsForm({form}) {
|
|
return <>
|
|
<InfoHeader
|
|
headerSize='h4'
|
|
headerContent='Videos Tags'
|
|
popupContent='Tag all Videos with these Tags.'
|
|
/>
|
|
<DownloadTagsSelector form={form}/>
|
|
</>
|
|
}
|
|
|
|
export function ChannelTagNameForm({form}) {
|
|
return <>
|
|
<InfoHeader
|
|
headerSize='h4'
|
|
headerContent='Channel Tag'
|
|
popupContent='If the Channel is new, apply this Tag.'
|
|
/>
|
|
<DownloadTagsSelector
|
|
form={form}
|
|
limit={1}
|
|
name='channel_tag_name'
|
|
path='settings.channel_tag_name'
|
|
/>
|
|
</>
|
|
}
|
|
|
|
export function UseBrowserProfile({form}) {
|
|
const [config, setConfig] = useState(null);
|
|
|
|
const localFetchConfig = async () => {
|
|
const result = await fetchVideoDownloaderConfig();
|
|
setConfig(result);
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
localFetchConfig();
|
|
}, []);
|
|
|
|
const popupContent = <>Use the browser profile to download videos.
|
|
This is useful for downloading videos that require a login. See: <i>Videos > Settings</i></>;
|
|
const label = <InfoHeader
|
|
headerSize='h4'
|
|
headerContent='Use Browser Profile'
|
|
popupContent={popupContent}
|
|
/>;
|
|
return <ToggleForm
|
|
form={form}
|
|
label={label}
|
|
disabled={!config?.browser_profile}
|
|
name='use_browser_profile'
|
|
path='settings.use_browser_profile'
|
|
/>
|
|
}
|
|
|
|
export function VideosDownloadForm({singleDownload = true, onCancel}) {
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
// Keep video settings in session to help user start downloads consistently.
|
|
const [defaultVideoResolutions, setDefaultVideoResolutions] = useLocalStorage('video_resolutions', defaultVideoResolutionOptions);
|
|
const [defaultVideoFormat, setDefaultVideoFormat] = useLocalStorage('video_format', defaultVideoFormatOption);
|
|
|
|
const defaultFormData = {
|
|
urls: '', // Textarea, one URL per line.
|
|
destination: '',
|
|
tag_names: [],
|
|
downloader: Downloaders.Video,
|
|
settings: {
|
|
use_browser_profile: false,
|
|
video_format: defaultVideoFormat,
|
|
video_resolutions: defaultVideoResolutions,
|
|
}
|
|
}
|
|
|
|
const submitter = async (formData) => {
|
|
const downloadData = {
|
|
destination: formData.destination,
|
|
downloader: formData.downloader,
|
|
settings: formData.settings,
|
|
tag_names: formData.tag_names,
|
|
urls: formData.urls.split(/r?\n/),
|
|
}
|
|
await postDownload(downloadData);
|
|
}
|
|
|
|
const onSuccess = () => {
|
|
setShowMessage(true);
|
|
form.reset();
|
|
}
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData,
|
|
onSuccess,
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
const {video_resolutions, video_format} = form.formData.settings;
|
|
if (video_resolutions && video_resolutions !== defaultVideoResolutions) {
|
|
setDefaultVideoResolutions(video_resolutions);
|
|
}
|
|
if (video_format && video_format !== defaultVideoFormat) {
|
|
setDefaultVideoFormat(video_format);
|
|
}
|
|
}, [form.formData]);
|
|
|
|
const localOnCancel = (e) => {
|
|
if (e) e.preventDefault();
|
|
if (onCancel) {
|
|
onCancel();
|
|
}
|
|
}
|
|
|
|
// This form can handle a single Video download, or multiple video downloads.
|
|
const urlInput = singleDownload ?
|
|
<UrlInput required form={form} path='urls'/>
|
|
: <UrlsTextarea required form={form}/>;
|
|
|
|
return <Form>
|
|
<Header as='h3'>
|
|
<Icon name='film' color='blue'/>
|
|
Videos
|
|
</Header>
|
|
<p>Download each video at the URLs provided below.</p>
|
|
|
|
<Grid stackable columns={1}>
|
|
<Grid.Row>
|
|
<Grid.Column>{urlInput}</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Row columns={1}>
|
|
<Grid.Column>
|
|
<VideoTagsForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<DestinationForm
|
|
form={form}
|
|
helpContent="Videos download into their Channel's directory, by default. If this is provided, then videos in this Channel/Playlist will download to this directory instead."
|
|
/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column width={11}>
|
|
<VideoResolutionSelectorForm form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column width={4}>
|
|
<VideoFormatSelectorForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UseBrowserProfile form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={1}>
|
|
<Grid.Column>
|
|
<ChannelTagNameForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<DownloadFormButtons onCancel={localOnCancel} form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
|
|
export function VideoMinimumDurationForm({form}) {
|
|
return <VideoDurationLimit
|
|
form={form}
|
|
path='settings.minimum_duration'
|
|
label='Minimum Duration'
|
|
name='minimum_duration'
|
|
helpContent='Download only Videos this many seconds long, or greater.'
|
|
placeholder='60'
|
|
/>
|
|
}
|
|
|
|
export function VideoMaximumDurationForm({form}) {
|
|
return <VideoDurationLimit
|
|
form={form}
|
|
path='settings.maximum_duration'
|
|
label='Maximum Duration'
|
|
name='maximum_duration'
|
|
helpContent='Do not download videos longer than this many seconds.'
|
|
placeholder='3600'
|
|
/>
|
|
}
|
|
|
|
|
|
export function ChannelDownloadForm({
|
|
download,
|
|
onCancel,
|
|
onSuccess,
|
|
onDelete = async () => {
|
|
},
|
|
submitter,
|
|
actions = null,
|
|
clearOnSuccess = true,
|
|
channel_id = null,
|
|
}) {
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
// May have received submitter from EditChannelDownloadForm.
|
|
submitter = submitter || (async (formData) => {
|
|
const downloadData = {
|
|
destination: formData.destination,
|
|
downloader: formData.downloader,
|
|
frequency: formData.frequency,
|
|
settings: formData.settings,
|
|
sub_downloader: formData.sub_downloader,
|
|
tag_names: formData.tag_names || [],
|
|
urls: [formData.url,],
|
|
}
|
|
await postDownload(downloadData);
|
|
});
|
|
|
|
const localOnSuccess = async () => {
|
|
setShowMessage(true);
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
// Keep video settings in session to help user start downloads consistently.
|
|
const [defaultVideoResolutions, setDefaultVideoResolutions] = useLocalStorage('video_resolutions', defaultVideoResolutionOptions);
|
|
const [defaultVideoFormat, setDefaultVideoFormat] = useLocalStorage('video_format', defaultVideoFormatOption);
|
|
|
|
const emptyFormData = {
|
|
destination: '',
|
|
downloader: Downloaders.VideoChannel,
|
|
frequency: days30Option.value,
|
|
url: '',
|
|
settings: {
|
|
channel_id,
|
|
channel_tag_name: [],
|
|
download_order: 'newest',
|
|
maximum_duration: null,
|
|
minimum_duration: null,
|
|
title_exclude: null,
|
|
title_include: null,
|
|
use_browser_profile: false,
|
|
video_count_limit: null,
|
|
video_format: defaultVideoFormat,
|
|
video_resolutions: defaultVideoResolutions,
|
|
},
|
|
sub_downloader: Downloaders.Video,
|
|
tag_names: [],
|
|
};
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData: mergeDeep(emptyFormData, download),
|
|
emptyFormData,
|
|
onSuccess: localOnSuccess,
|
|
clearOnSuccess,
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
const {video_resolutions, video_format} = form.formData.settings;
|
|
if (video_resolutions && video_resolutions !== defaultVideoResolutions) {
|
|
setDefaultVideoResolutions(video_resolutions);
|
|
}
|
|
if (video_format && video_format !== defaultVideoFormat) {
|
|
setDefaultVideoFormat(video_format);
|
|
}
|
|
}, [form.formData]);
|
|
|
|
const onceMessage = <Message>
|
|
<Message.Header>Download Once</Message.Header>
|
|
<Message.Content>You have selected a frequency of Once, this is useful when you want to download
|
|
all videos in a Playlist, and when you do not want to download any videos added to the playlist
|
|
in the future.</Message.Content>
|
|
</Message>;
|
|
|
|
// Default to "new" download buttons.
|
|
actions = actions || DownloadFormButtons;
|
|
const actionsElm = actions({onDelete, onCancel, form});
|
|
|
|
return <Form>
|
|
<Header as='h3'><Icon name='film' color='blue'/> Channel / Playlist</Header>
|
|
|
|
<Grid stackable columns={1}>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UrlInput required form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={1}>
|
|
<Grid.Column>
|
|
<VideoTagsForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column mobile={4} tablet={4}>
|
|
<DownloadFrequencySelector form={form} freqOptions={channelFrequencyOptions}/>
|
|
</Grid.Column>
|
|
<Grid.Column mobile={4} tablet={12}>
|
|
<DestinationForm
|
|
form={form}
|
|
helpContent='Destination is not required. Videos will download into the automatically created Channel directory.'
|
|
/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{form.formData.frequency === 0 && <Grid.Row><Grid.Column>{onceMessage}</Grid.Column></Grid.Row>}
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column>
|
|
<TitleInclusionInput form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column>
|
|
<TitleExclusionInput form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column mobile={4} tablet={5}>
|
|
<VideoDownloadOrder form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column mobile={4} tablet={5}>
|
|
<VideoDownloadCountLimit form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column width={11}>
|
|
<VideoResolutionSelectorForm form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column width={4}>
|
|
<VideoFormatSelectorForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column tablet={8} computer={4}>
|
|
<VideoMinimumDurationForm form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column tablet={8} computer={4}>
|
|
<VideoMaximumDurationForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UseBrowserProfile form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={1}>
|
|
<Grid.Column>
|
|
<ChannelTagNameForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='right'>
|
|
{actionsElm}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
export function EditChannelDownloadForm({
|
|
download,
|
|
onCancel,
|
|
onSuccess,
|
|
onDelete,
|
|
withTags = true,
|
|
actions = EditDownloadFormButtons,
|
|
}) {
|
|
|
|
const submitter = async (formData) => {
|
|
const downloadData = {
|
|
destination: formData.destination,
|
|
downloader: formData.downloader,
|
|
frequency: formData.frequency,
|
|
settings: formData.settings,
|
|
tag_names: formData.tag_names || [],
|
|
urls: [formData.url,],
|
|
}
|
|
await putDownload(download.id, downloadData);
|
|
}
|
|
|
|
return <ChannelDownloadForm
|
|
download={download}
|
|
submitter={submitter}
|
|
onCancel={onCancel}
|
|
onSuccess={onSuccess}
|
|
onDelete={onDelete}
|
|
withTags={withTags}
|
|
actions={actions}
|
|
clearOnSuccess={false}
|
|
/>
|
|
}
|
|
|
|
function SuccessfulDownloadSubmitMessage() {
|
|
return <Grid.Row>
|
|
<Grid.Column><Message positive>
|
|
<Message.Header>Download Submitted</Message.Header>
|
|
<Message.Content>
|
|
<Link to='/admin'><Icon name='checkmark'/> View downloads</Link>
|
|
</Message.Content>
|
|
</Message>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
}
|
|
|
|
export function ArchiveDownloadForm({download, onCancel}) {
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
const submitter = async (formData) => {
|
|
const downloadData = {
|
|
downloader: formData.downloader,
|
|
tag_names: formData.tag_names,
|
|
urls: formData.urls.split(/\r?\n/),
|
|
}
|
|
await postDownload(downloadData);
|
|
};
|
|
|
|
const emptyFormData = {
|
|
downloader: Downloaders.Archive,
|
|
urls: '',
|
|
tag_names: [],
|
|
};
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData: mergeDeep(emptyFormData, download),
|
|
emptyFormData,
|
|
clearOnSuccess: true,
|
|
onSuccess: async () => setShowMessage(true),
|
|
});
|
|
|
|
return <Form>
|
|
<Header as='h3'><Icon name='file text' color='green'/> Archives</Header>
|
|
<p>Create a Singlefile Archive for each of the URLs provided below.</p>
|
|
|
|
<Grid stackable columns={1}>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UrlsTextarea required form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<DownloadTagsSelector form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='right'>
|
|
<DownloadFormButtons onCancel={onCancel} form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
export function RSSDownloadForm({download, submitter, onDelete, onCancel, actions, clearOnSuccess = true}) {
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
const [defaultVideoResolutions, setDefaultVideoResolutions] = useLocalStorage('video_resolutions', defaultVideoResolutionOptions);
|
|
const [defaultVideoFormat, setDefaultVideoFormat] = useLocalStorage('video_format', defaultVideoFormatOption);
|
|
|
|
submitter = submitter || (async (formData) => {
|
|
const downloadData = {
|
|
destination: formData.destination,
|
|
downloader: formData.downloader,
|
|
frequency: formData.frequency,
|
|
settings: formData.settings,
|
|
sub_downloader: formData.sub_downloader,
|
|
tag_names: formData.tag_names || [],
|
|
urls: [formData.url],
|
|
}
|
|
await postDownload(downloadData);
|
|
});
|
|
|
|
const emptyFormData = {
|
|
destination: null,
|
|
downloader: Downloaders.RSS,
|
|
frequency: weeklyOption.value,
|
|
sub_downloader: null,
|
|
settings: {
|
|
excluded_urls: null,
|
|
title_exclude: null,
|
|
title_include: null,
|
|
video_resolutions: defaultVideoResolutions,
|
|
video_format: defaultVideoFormat,
|
|
},
|
|
tag_names: [],
|
|
url: '',
|
|
};
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData: mergeDeep(emptyFormData, download),
|
|
emptyFormData,
|
|
clearOnSuccess,
|
|
onSuccess: async () => setShowMessage(true),
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
const {video_resolutions, video_format} = form.formData.settings;
|
|
if (video_resolutions && video_resolutions !== defaultVideoResolutions) {
|
|
setDefaultVideoResolutions(video_resolutions);
|
|
}
|
|
if (video_format && video_format !== defaultVideoFormat) {
|
|
setDefaultVideoFormat(video_format);
|
|
}
|
|
}, [form.formData]);
|
|
|
|
// Default to "new" download buttons.
|
|
actions = actions || DownloadFormButtons;
|
|
const actionsElm = actions({onDelete, onCancel, form});
|
|
|
|
let downloaderRows;
|
|
if (form.formData.sub_downloader === Downloaders.Video) {
|
|
downloaderRows = <>
|
|
<Grid.Row columns={1}>
|
|
<Grid.Column>
|
|
<DestinationForm
|
|
form={form}
|
|
helpContent="Videos download into their Channel's directory, by default. If this is provided, then videos in this feed will download to this directory instead."
|
|
/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column mobile={16} computer={8}>
|
|
<TitleInclusionInput form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column mobile={16} computer={8}>
|
|
<TitleExclusionInput form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column mobile={16} computer={11}>
|
|
<VideoResolutionSelectorForm form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column mobile={8} computer={3}>
|
|
<VideoFormatSelectorForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column tablet={8} computer={4}>
|
|
<VideoMinimumDurationForm form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column tablet={8} computer={4}>
|
|
<VideoMaximumDurationForm form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</>;
|
|
} else if (form.formData.sub_downloader === Downloaders.Archive) {
|
|
downloaderRows = <Grid.Row>
|
|
<Grid.Column>
|
|
<ExcludedUrls form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>;
|
|
}
|
|
|
|
return <Form>
|
|
<Header as='h3'><Icon name='rss' color='orange'/> RSS Feed</Header>
|
|
<p>Download each link provided by this RSS feed using the selected downloader.</p>
|
|
|
|
<Grid columns={1}>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UrlInput required form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<DownloadTagsSelector form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column width={8}>
|
|
<DownloadFrequencySelector form={form} freqOptions={extendedFrequencyOptions}/>
|
|
</Grid.Column>
|
|
<Grid.Column width={8}>
|
|
<DownloaderSelector form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{downloaderRows}
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='right'>
|
|
{actionsElm}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
export function EditRSSDownloadForm({download, onDelete, onCancel, actions = EditDownloadFormButtons}) {
|
|
|
|
const submitter = async (formData) => {
|
|
const downloadData = {
|
|
destination: formData.destination,
|
|
downloader: formData.downloader,
|
|
frequency: formData.frequency,
|
|
settings: formData.settings,
|
|
sub_downloader: formData.sub_downloader,
|
|
tag_names: formData.tag_names || [],
|
|
urls: [formData.url,],
|
|
}
|
|
await putDownload(download.id, downloadData);
|
|
}
|
|
|
|
return <RSSDownloadForm
|
|
submitter={submitter}
|
|
onDelete={onDelete}
|
|
onCancel={onCancel}
|
|
download={download}
|
|
actions={actions}
|
|
clearOnSuccess={false}
|
|
/>
|
|
}
|
|
|
|
export function EditZimDownloadForm({download, onDelete, onCancel, actions = EditDownloadFormButtons}) {
|
|
|
|
const submitter = async (formData) => {
|
|
const downloadData = {
|
|
downloader: formData.downloader,
|
|
frequency: formData.frequency,
|
|
settings: formData.settings,
|
|
sub_downloader: formData.sub_downloader,
|
|
tag_names: formData.tag_names,
|
|
urls: [formData.url,],
|
|
}
|
|
await putDownload(download.id, downloadData);
|
|
}
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
const emptyFormData = {
|
|
downloader: Downloaders.RSS,
|
|
frequency: weeklyOption.value,
|
|
sub_downloader: null,
|
|
tag_names: [],
|
|
url: '',
|
|
};
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData: mergeDeep(emptyFormData, download),
|
|
emptyFormData,
|
|
onSuccess: async () => setShowMessage(true),
|
|
});
|
|
|
|
// Default to "new" download buttons.
|
|
actions = actions || DownloadFormButtons;
|
|
const actionsElm = actions({onDelete, onCancel, form});
|
|
|
|
return <Form>
|
|
<Header as='h3'>Zim File</Header>
|
|
|
|
<Grid stackable columns={1}>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UrlInput required form={form} disabled={true}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column width={8}>
|
|
<DownloadFrequencySelector form={form} longFrequenciesAvailable={true}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='right'>
|
|
{actionsElm}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
export function FilesDownloadForm({
|
|
download,
|
|
submitter,
|
|
onCancel,
|
|
actions = DownloadFormButtons,
|
|
clearOnSuccess = true,
|
|
}) {
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
submitter = submitter || (async (formData) => {
|
|
const downloadData = {
|
|
downloader: Downloaders.File,
|
|
tag_names: formData.tag_names,
|
|
destination: formData.destination,
|
|
urls: formData.urls.split(/\r?\n/),
|
|
}
|
|
await postDownload(downloadData);
|
|
});
|
|
|
|
const emptyFormData = {
|
|
destination: null,
|
|
tag_names: [],
|
|
urls: '',
|
|
};
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData: mergeDeep(emptyFormData, download),
|
|
emptyFormData,
|
|
clearOnSuccess,
|
|
onSuccess: async () => setShowMessage(true),
|
|
});
|
|
|
|
const actionsElm = actions({onCancel, form});
|
|
|
|
return <Form>
|
|
<Header as='h3'><Icon name='file'/> Files</Header>
|
|
<p>Download each file at the URLs provided below.</p>
|
|
|
|
<Grid>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<UrlsTextarea form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<DownloadTagsSelector form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
<DestinationForm required form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column textAlign='right'>
|
|
{actionsElm}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
const suffixValidator = (value) => {
|
|
const error = commaSeparatedValidator(value);
|
|
if (error) {
|
|
return error;
|
|
}
|
|
|
|
const suffixes = value.split(',');
|
|
for (const suffix of suffixes) {
|
|
if (!suffix.startsWith('.')) {
|
|
return 'Suffix must start with .';
|
|
}
|
|
if (suffix.length === 1) {
|
|
return 'Suffix must have characters after .';
|
|
}
|
|
}
|
|
}
|
|
|
|
export function SuffixFormInput({form, name = 'suffix', path = 'settings.suffix'}) {
|
|
return <InputForm
|
|
form={form}
|
|
label='File Suffixes'
|
|
helpContent='Comma-separated list of file suffixes that should be downloaded'
|
|
required={true}
|
|
placeholder='.pdf,.mp4'
|
|
name={name}
|
|
path={path}
|
|
validator={suffixValidator}
|
|
/>
|
|
}
|
|
|
|
export function ScrapeFilesDownloadForm({
|
|
download,
|
|
submitter,
|
|
clearOnSuccess,
|
|
onDelete,
|
|
onCancel,
|
|
onSuccess,
|
|
actions = DownloadFormButtons,
|
|
singleDownload = false,
|
|
}) {
|
|
const [showMessage, setShowMessage] = React.useState(false);
|
|
|
|
submitter = submitter || (async (formData) => {
|
|
const downloadData = {
|
|
downloader: Downloaders.ScrapeHtml,
|
|
sub_downloader: Downloaders.File,
|
|
tag_names: formData.tag_names || [],
|
|
destination: formData.destination,
|
|
urls: formData.urls.split(/\r?\n/),
|
|
settings: formData.settings,
|
|
}
|
|
await postDownload(downloadData);
|
|
});
|
|
|
|
const emptyFormData = {
|
|
destination: null,
|
|
tag_names: [],
|
|
urls: '',
|
|
settings: {
|
|
depth: 1,
|
|
max_pages: 1,
|
|
suffix: '',
|
|
}
|
|
};
|
|
|
|
const localOnSuccess = async () => {
|
|
setShowMessage(true);
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
}
|
|
}
|
|
|
|
const form = useForm({
|
|
submitter,
|
|
defaultFormData: mergeDeep(emptyFormData, download),
|
|
emptyFormData,
|
|
clearOnSuccess,
|
|
onSuccess: localOnSuccess,
|
|
});
|
|
|
|
const urlInput = singleDownload ?
|
|
<UrlInput required form={form} path='url'/>
|
|
: <UrlsTextarea required form={form}/>;
|
|
|
|
// Default to "new" download buttons.
|
|
actions = actions || DownloadFormButtons;
|
|
const actionsElm = actions({onDelete, onCancel, form});
|
|
|
|
return <Form>
|
|
<Header as='h3'><Icon name='file alternate' color='red'/> Scrape Files</Header>
|
|
<p>Search each of the URLs for files matching the suffix (.pdf, etc.).</p>
|
|
|
|
<Grid>
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
{urlInput}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column width={10}>
|
|
<DestinationForm required={true} form={form}/>
|
|
</Grid.Column>
|
|
<Grid.Column width={6}>
|
|
<SuffixFormInput required={true} form={form}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
<Grid.Row columns={2}>
|
|
<Grid.Column width={4}>
|
|
<DepthInputForm form={form} required={true}/>
|
|
</Grid.Column>
|
|
<Grid.Column width={4}>
|
|
<MaximumPagesInputForm form={form} required={true}/>
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
{showMessage && <SuccessfulDownloadSubmitMessage/>}
|
|
<Grid.Row>
|
|
<Grid.Column>
|
|
{actionsElm}
|
|
</Grid.Column>
|
|
</Grid.Row>
|
|
</Grid>
|
|
</Form>
|
|
}
|
|
|
|
export function EditScrapeFilesDownloadForm({download, onDelete, onCancel, onSuccess}) {
|
|
const submitter = async (formData) => {
|
|
const downloadData = {
|
|
downloader: Downloaders.ScrapeHtml,
|
|
sub_downloader: Downloaders.File,
|
|
tag_names: formData.tag_names || [],
|
|
destination: formData.destination,
|
|
urls: [formData.url],
|
|
settings: formData.settings,
|
|
}
|
|
await postDownload(downloadData);
|
|
};
|
|
|
|
return <ScrapeFilesDownloadForm
|
|
download={download}
|
|
submitter={submitter}
|
|
onDelete={onDelete}
|
|
onCancel={onCancel}
|
|
onSuccess={onSuccess}
|
|
actions={EditDownloadFormButtons}
|
|
singleDownload={true}
|
|
/>
|
|
}
|
|
|
|
export function DownloadMenu({onOpen, disabled}) {
|
|
const [downloader, setDownloader] = useState();
|
|
|
|
const localOnOpen = (name) => {
|
|
setDownloader(name);
|
|
if (onOpen) {
|
|
onOpen(name);
|
|
}
|
|
}
|
|
|
|
let body = (<>
|
|
<Button
|
|
color='blue'
|
|
content='Videos'
|
|
disabled={disabled}
|
|
onClick={() => localOnOpen('video')}
|
|
style={{marginBottom: '1em'}}
|
|
/>
|
|
<Button
|
|
color='green'
|
|
content='Archives'
|
|
disabled={disabled}
|
|
onClick={() => localOnOpen('archive')}
|
|
style={{marginBottom: '1em'}}
|
|
/>
|
|
<Button
|
|
color='blue'
|
|
content='Channel/Playlist'
|
|
disabled={disabled}
|
|
onClick={() => localOnOpen('video_channel')}
|
|
style={{marginBottom: '1em'}}
|
|
/>
|
|
<Button
|
|
content='RSS Feed'
|
|
disabled={disabled}
|
|
onClick={() => localOnOpen('rss')}
|
|
style={{marginBottom: '1em'}}
|
|
/>
|
|
<Button
|
|
color='black'
|
|
content='Files'
|
|
disabled={disabled}
|
|
onClick={() => localOnOpen('file')}
|
|
style={{marginBottom: '1em'}}
|
|
/>
|
|
<Button
|
|
color='red'
|
|
content='Scrape'
|
|
disabled={disabled}
|
|
onClick={() => localOnOpen('scrape')}
|
|
style={{marginBottom: '1em'}}
|
|
/>
|
|
</>);
|
|
|
|
function clearSelected() {
|
|
localOnOpen(null);
|
|
body = null;
|
|
}
|
|
|
|
const downloaders = {
|
|
archive: <ArchiveDownloadForm onCancel={clearSelected}/>,
|
|
video: <VideosDownloadForm singleDownload={false} onCancel={clearSelected}/>,
|
|
video_channel: <ChannelDownloadForm onCancel={clearSelected}/>,
|
|
rss: <RSSDownloadForm onCancel={clearSelected}/>,
|
|
file: <FilesDownloadForm onCancel={clearSelected}/>,
|
|
scrape: <ScrapeFilesDownloadForm onCancel={clearSelected}/>
|
|
};
|
|
|
|
if (downloader in downloaders) {
|
|
const downloaderForm = downloaders[downloader];
|
|
body = <>
|
|
{downloaderForm}
|
|
</>
|
|
}
|
|
|
|
return <>
|
|
{body}
|
|
</>
|
|
}
|