import React, {useState} from "react"; import {Dimmer, Dropdown, Grid, Label, StatisticLabel, StatisticValue, TableCell, TableRow,} from "semantic-ui-react"; import { createChannel, createChannelDownload, deleteChannel, deleteDownload, refreshChannel, tagChannel, tagChannelInfo, updateChannel, updateChannelDownload } from "../api"; import { APIButton, BackButton, DirectoryInput, ErrorMessage, frequencyOptions, HelpHeader, humanFileSize, RequiredAsterisk, SearchInput, secondsToFrequency, secondsToFullDuration, Toggle, useTitle, validURL, WROLModeMessage } from "./Common"; import {Link, useNavigate, useParams} from "react-router-dom"; import Message from "semantic-ui-react/dist/commonjs/collections/Message"; import {useChannel, useChannels, useOneQuery, useWROLMode} from "../hooks/customHooks"; import _ from "lodash"; import { Button, Form, FormField, FormGroup, FormInput, Header, Loader, Modal, ModalActions, ModalContent, ModalHeader, Segment, Statistic } from "./Theme"; import {Media, ThemeContext} from "../contexts/contexts"; import {SortableTable} from "./SortableTable"; import {toast} from "react-semantic-toasts-2"; import {RecurringDownloadsTable} from "./admin/Downloads"; import {TagsContext, TagsSelector} from "../Tags"; function ChannelStatistics({statistics}) { if (!statistics) { return <> } return
Statistics
{statistics.video_count} Videos {humanFileSize(statistics.size, true)} Total Size {humanFileSize(statistics.largest_video, true)} Largest Video {secondsToFullDuration(statistics.length)} Total Duration
} function ChannelPage({create, header}) { const [disabled, setDisabled] = useState(false); const [errors, setErrors] = useState({}); const [error, setError] = useState(false); const [success, setSuccess] = useState(false); const [messageHeader, setMessageHeader] = useState(); const [messageContent, setMessageContent] = useState(); const [downloadModalOpen, setDownloadModalOpen] = useState(false); const navigate = useNavigate(); const {channelId} = useParams(); const {channel, changeValue, fetchChannel} = useChannel(channelId); const {SingleTag} = React.useContext(TagsContext); const [tagModalOpen, setTagModalOpen] = useState(false); const [newTagName, setNewTagName] = useState(channel ? channel.tag_name : null); const [moveToTagDirectory, setMoveToTagDirectory] = useState(true); const [newTagDirectory, setNewTagDirectory] = useState(''); useTitle(_.isEmpty(channel) ? null : `${channel.name} Channel`); React.useEffect(() => { console.debug(channel); if (!_.isEmpty(channel)) { setNewTagName(channel.tag_name); } }, [JSON.stringify(channel)]); if (_.isEmpty(channel)) { return ; } const handleCheckbox = (e, {name, checked}) => { if (e) { e.preventDefault(); } changeValue(name, checked); } const handleInputChange = (e, {name, value}) => { if (e) { e.preventDefault(); } changeValue(name, value); } const setErrorMessage = (header, message) => { setError(true); setSuccess(false); setMessageHeader(header); setMessageContent(message); } const setSuccessMessage = (header, message) => { setError(false); setSuccess(true); setMessageHeader(header); setMessageContent(message); } const handleSubmit = async (e) => { if (e) { e.preventDefault(); } const body = { name: channel.name, directory: channel.directory, mkdir: channel.mkdir, url: channel.url, }; setDisabled(true); let response = null; try { if (create !== undefined) { response = await createChannel(body); } else { response = await updateChannel(channelId, body); } } catch (e) { console.error(e); toast({ type: 'error', title: 'Unexpected server response', description: 'Could not save channel', time: 5000, }); return; } finally { setDisabled(false); } if (response && response.status >= 200 && response.status < 300) { let location = response.headers.get('Location'); let channelResponse = await fetch(location); let data = await channelResponse.json(); let channel = data['channel']; if (response.status === 201) { setSuccessMessage( 'Channel created', Your channel was created. View it here ); } else { await fetchChannel(); toast({ type: 'success', title: 'Channel updated', description: 'The channel was updated', }) } } else if (response) { // Some error occurred. let error = await response.json(); let cause = error.cause; if (error.code === 'UNKNOWN_DIRECTORY' || (cause && cause.code === 'UNKNOWN_DIRECTORY')) { setErrorMessage( 'Invalid directory', 'This directory does not exist.', 'directory', ); } else if (cause && cause.code === 'CHANNEL_DIRECTORY_CONFLICT') { setErrorMessage( 'Invalid directory', 'This directory is already used by another channel', 'directory', ); } else if (cause && cause.code === 'CHANNEL_NAME_CONFLICT') { setErrorMessage( 'Invalid name', 'This channel name is already taken', 'name', ); } else { setErrorMessage('Invalid channel', 'Unable to save channel. See logs.'); } } else { console.error('Did not get a response for channel!'); } } const handleRefreshChannel = async (e) => { if (e) { e.preventDefault(); } const response = await refreshChannel(channelId); if (response.status !== 204) { toast({ type: 'error', title: 'Failed to refresh', description: "Failed to refresh this channel's directory", time: 5000, }) } } const handleDelete = async () => { try { let response = await deleteChannel(channelId); if (response.status === 204) { navigate('/videos/channel'); } } catch (e) { setErrorMessage('Failed to delete', 'Failed to delete this channel, check logs.'); } } let downloads = channel && channel.downloads ? channel.downloads : null; const afterNewDownloadSave = async () => { await fetchChannel(); setDownloadModalOpen(false); } const handleTagModalOpen = (e) => { if (e) { e.preventDefault(); } setTagModalOpen(true); } const handleTagChannel = async () => { try { await tagChannel(channelId, newTagName, moveToTagDirectory ? newTagDirectory : null); setTagModalOpen(false); } catch (e) { console.error('Failed to tag channel', e); } finally { await fetchChannel(); } } const handleTagSelect = async (newTagName_) => { setNewTagName(newTagName_); try { const videosDestination = await tagChannelInfo(channelId, newTagName_); setNewTagDirectory(videosDestination); } catch (e) { console.error('Failed to tag channel', e); } } const tagModal = setTagModalOpen(false)} closeIcon > {channel.tag_name ? 'Modify Tag' : 'Add Tag'} handleTagSelect(null)} />
{newTagDirectory && }
Save
; return <> {!create && }
{header}
changeValue('name', value)} /> changeValue('directory', value)} placeholder='videos/channel directory' /> { create !== undefined && {/* Empty field*/} { handleCheckbox(null, {name: 'mkdir', checked: i}) }} /> } {channel.tag_name && } {!create && <> Delete Refresh } {tagModal} Save
{!create &&
Downloads
setDownloadModalOpen(false)}>
New Channel Download
setDownloadModalOpen(false)} />
}
{channel.statistics && }
} export function ChannelDownloadForm({channel_id, afterSave, closeModal, download}) { download = download || {}; channel_id = download ? download.channel_id || channel_id : channel_id; const editing = download && !_.isEmpty(download); const settings = download.settings ? download.settings : {}; const oldTagNames = download && download.settings && download.settings.tag_names ? download.settings.tag_names : []; const [state, setState] = React.useState({ frequency: download.frequency ? download.frequency : 604800, title_exclude: settings.title_exclude ? settings.title_exclude : '', title_include: settings.title_include ? settings.title_include : '', url: download ? download.url : '', }) const [disabled, setDisabled] = React.useState(useWROLMode()); const [loading, setLoading] = React.useState(false); const [urlValid, setUrlValid] = React.useState(true); const [tagNames, setTagNames] = React.useState(oldTagNames); const handleInputChange = (e, {name, value}) => { if (e) { e.preventDefault(); } if (name === 'url') { setUrlValid(validURL(value)); } setState({...state, [name]: value}); } const handleSubmit = async () => { setLoading(true); setDisabled(true); try { let response; if (editing) { response = await updateChannelDownload( channel_id, download.id, state.url, state.frequency, state.title_include, state.title_exclude, tagNames, ); } else { response = await createChannelDownload( channel_id, state.url, state.frequency, state.title_include, state.title_exclude, tagNames, ); } if (!response.ok) { throw 'Creating download failed'; } if (afterSave) { await afterSave(); } } finally { setLoading(false); setDisabled(false); } } const handleClose = (e) => { if (e) e.preventDefault(); closeModal(); } const handleDelete = async () => { await deleteDownload(download.id); if (afterSave) { afterSave(); } if (closeModal) { closeModal(); } }; const deleteDownloadButton = Delete; return
{loading && } setTagNames(i)}/> {editing && deleteDownloadButton}
} export function ChannelEditPage(props) { return ; } export function ChannelNewPage(props) { useTitle('New Channel'); return } function ChannelRow({channel}) { const {SingleTag} = React.useContext(TagsContext); const videosTo = `/videos/channel/${channel.id}/video`; const {inverted} = React.useContext(ThemeContext); const editTo = `/videos/channel/${channel.id}/edit`; const buttonClass = `ui button secondary ${inverted}`; return {channel.name} {channel.tag_name ? : null} {channel.video_count} {channel.url && channel.minimum_frequency ? secondsToFrequency(channel.minimum_frequency) : null} {channel.size ? humanFileSize(channel.size) : null} Edit ; } function MobileChannelRow({channel}) { const {SingleTag} = React.useContext(TagsContext); const editTo = `/videos/channel/${channel.id}/edit`; const videosTo = `/videos/channel/${channel.id}/video`; return

{channel.name}

{channel.tag_name ? : null}

Videos: {channel.video_count}

Edit

; } export function ChannelsPage() { useTitle('Channels'); const {channels} = useChannels(); const [searchStr, setSearchStr] = useOneQuery('name'); // Hides Channels with few videos. const [hideSmall, setHideSmall] = React.useState(true); const enoughChannelsToHideSmall = channels && channels.length > 20; const header =
setSearchStr('')} onChange={setSearchStr} onSubmit={null} />
; const headers = [ {key: 'name', text: 'Name', sortBy: 'name', width: 8}, {key: 'tag', text: 'Tag', sortBy: 'tag_name', width: 2}, {key: 'video_count', text: 'Videos', sortBy: 'video_count', width: 2}, {key: 'download_frequency', text: 'Download Frequency', sortBy: 'minimum_frequency', width: 2}, {key: 'size', text: 'Size', sortBy: 'size', width: 2}, {key: 'manage', text: 'Manage', width: 2}, ]; const mobileHeaders = [ {key: 'name', text: 'Name', sortBy: 'name'}, {key: 'video_count', text: 'Videos', sortBy: 'video_count'}, {key: 'manage', text: 'Manage'}, ]; if (channels && channels.length === 0) { return <> {header} No channels exist yet! Create one. } else if (channels === undefined) { return <> {header} Could not fetch Channels } let filteredChannels = channels; if (searchStr && Array.isArray(channels)) { const re = new RegExp(_.escapeRegExp(searchStr), 'i'); filteredChannels = channels.filter(i => re.test(i['name'])); } else if (channels && hideSmall && enoughChannelsToHideSmall) { // Get the top 80% of Channels. let index80 = Math.floor(channels.length * 0.8); const sortedChannels = channels.sort((a, b) => b.video_count - a.video_count); let percentile = sortedChannels[index80].video_count; filteredChannels = channels.filter(i => { return i.video_count > percentile || i.name.toLowerCase() === 'wrolpi' }); if (filteredChannels.length < 21) { // Filtering hid too many Channels, show them all. filteredChannels = channels; } } return <> {header} } /> } /> }