import React, {useState} from "react"; import {useHotkeys} from "react-hotkeys-hook"; import {createChannel, deleteChannel, refreshChannel, tagChannel, tagChannelInfo} from "../api"; import {CollectionTagModal} from "./collections/CollectionTagModal"; import {CollectionReorganizeModal} from "./collections/CollectionReorganizeModal"; import { APIButton, BackButton, ErrorMessage, humanFileSize, humanNumber, InfoPopup, SearchInput, secondsToFullDuration, SimpleAccordion, useTitle, WROLModeMessage } from "./Common"; import {Link, useNavigate, useParams} from "react-router"; import {useChannel, useChannels, useOneQuery} from "../hooks/customHooks"; import _ from "lodash"; import { Button, Grid, Group, Header, Loading, Message, Modal, Panel, Stack, Statistic, StatisticGroup, toast, } from "./ui"; import {RecurringDownloadsTable} from "./admin/Downloads"; import {InputForm, ToggleForm} from "../hooks/useForm"; import {ChannelDownloadForm, DestinationForm, DownloadTagsSelector} from "./Download"; import {CollectionTable} from "./collections/CollectionTable"; import {CollectionEditForm} from "./collections/CollectionEditForm"; // Channel table column configuration const CHANNEL_COLUMNS = [ {key: 'name', label: 'Name', sortable: true, width: 7}, {key: 'tag_name', label: 'Tag', sortable: true, width: 2}, {key: 'video_count', label: 'Videos', sortable: true, align: 'right', width: 2}, { key: 'min_download_frequency', label: 'Download Frequency', sortable: true, format: 'frequency', width: 2, hideOnMobile: true }, {key: 'total_size', label: 'Size', sortable: true, align: 'right', format: 'bytes', width: 2, hideOnMobile: true}, {key: 'actions', label: 'Manage', sortable: false, type: 'actions', width: 1} ]; const CHANNEL_ROUTES = { list: '/videos/channel', edit: '/videos/channel/:id/edit', search: '/videos/channel/:id/video', id_field: 'channel_id' }; function ChannelStatistics({statistics}) { if (!statistics) { return <>> } return Statistics } export function ChannelEditPage() { const navigate = useNavigate(); const {channelId} = useParams(); const [tagEditModalOpen, setTagEditModalOpen] = useState(false); const [downloadModalOpen, setDownloadModalOpen] = useState(false); const [reorganizeModalOpen, setReorganizeModalOpen] = useState(false); const {channel, form, fetchChannel} = useChannel(channelId); useTitle(_.isEmpty(channel) ? null : `${channel.name} Channel`); if (!form.ready) { return ; } const handleRefreshChannel = async (e) => { if (e) { e.preventDefault(); } await refreshChannel(channelId); } const handleDelete = async () => { try { let response = await deleteChannel(channelId); if (response.status === 204) { navigate('/videos/channel'); } } catch (e) { console.error('Failed to delete channel', e); } } // Handler for tag modal save const handleTagSave = async (tagName, directory) => { try { await tagChannel(channelId, tagName, directory); toast({ type: 'success', title: 'Channel Tagged', description: `Channel has been tagged with "${tagName}"`, time: 3000, }); } catch (e) { console.error('Failed to tag channel', e); } finally { setTimeout(async () => { await fetchChannel(); }, 500); } }; // Handler for fetching tag info const handleGetTagInfo = async (tagName) => { return await tagChannelInfo(channelId, tagName); }; const afterNewDownloadSave = async () => { setDownloadModalOpen(false); await form.fetcher(); } const onDelete = async () => { await form.fetcher(); setDownloadModalOpen(false); } const handleSubmit = async (e) => { if (e) e.preventDefault(); try { await form.onSubmit(); toast({ type: 'success', title: 'Channel Updated', description: 'Channel was successfully updated', time: 3000, }); await fetchChannel(); } catch (e) { console.error('Failed to update channel:', e); } }; const deleteButton = Delete; const refreshButton = Refresh; const tagButton = setTagEditModalOpen(true)} role='primary' style={{marginTop: '1em'}} >Tag; const reorganizeButton = ( setReorganizeModalOpen(true)} obeyWROLMode={true} style={{marginTop: '1em'}} >Reorganize Files ); const actionButtons = <> {deleteButton} {refreshButton} {tagButton} {reorganizeButton} >; const downloadMissingDataInfo = 'Automatically download missing comments, etc, in the background.'; const downloadMissingDataLabel = <>Download Missing Data>; return <> Videos {channel?.needs_reorganization && ( The file name format has changed. Click "Reorganize Files" to move existing files to match the new format. )} {(channel.url || channel.rss_url) && {channel.url && URL {channel.url} } {channel.rss_url && RSS URL {channel.rss_url} } } {/* Tag Modal */} setTagEditModalOpen(false)} currentTagName={channel.tag_name} originalDirectory={channel.directory} getTagInfo={handleGetTagInfo} onSave={handleTagSave} collectionName="Channel" hasDirectory={!!channel.directory} /> {/* Reorganize Modal */} setReorganizeModalOpen(false)} collectionId={channel?.collection_id} collectionName={channel?.name} onComplete={fetchChannel} needsReorganization={channel?.needs_reorganization} /> {/* Downloads Segment */} Downloads setDownloadModalOpen(!downloadModalOpen)}> New Download setDownloadModalOpen(false)}> New Channel Download setDownloadModalOpen(false)} onDelete={onDelete} /> {channel && channel.statistics && } >; } export function ChannelNewPage() { useTitle('New Channel'); // Used to display messages to maintainer. const [error, setError] = useState(false); const [success, setSuccess] = useState(false); const [messageHeader, setMessageHeader] = useState(); const [messageContent, setMessageContent] = useState(); const {channel, form} = useChannel(null); 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 () => { const body = { name: channel.name, directory: channel.directory, url: channel.url, download_missing_data: channel.download_missing_data, tag_name: _.isEmpty(channel.tag_name) ? null : channel.tag_name[0], }; let response = null; try { response = await createChannel(body); } catch (e) { console.error(e); toast({ type: 'error', title: 'Unexpected server response', description: 'Could not save channel', time: 5000, }); return; } if (response && response.ok) { let location = response.headers.get('Location'); let channelResponse = await fetch(location); let data = await channelResponse.json(); let newChannel = data['channel']; setSuccessMessage( 'Channel created', Your channel was created. View it here ); } else if (response) { let error = await response.json(); let cause = error.cause; if (cause && cause.code === 'CHANNEL_DIRECTORY_CONFLICT') { setErrorMessage( 'Invalid directory', 'This directory is already used by another channel', ); } else if (cause && cause.code === 'CHANNEL_NAME_CONFLICT') { setErrorMessage( 'Invalid name', 'This channel name is already taken', ); } else { setErrorMessage('Invalid channel', error.message || error.error || 'Unable to save channel. See logs.'); } } else { console.error('Did not get a response for channel!'); } } const downloadMissingDataInfo = 'Automatically download missing comments, etc, in the background.'; const downloadMissingDataLabel = <>Download Missing Data>; let messageRow; if (error || success) { messageRow = {error && {messageContent}} {success && {messageContent}} } return <> New Channel e.preventDefault()} > {messageRow} Save >; } export function ChannelsPage() { useTitle('Channels'); const [channels] = useChannels(); const [searchStr, setSearchStr] = useOneQuery('name'); const searchInputRef = React.useRef(); useHotkeys('f', (e) => { e.preventDefault(); if (searchInputRef.current) { searchInputRef.current.focus(); } }, {enableOnFormTags: false}); // Header section matching DomainsPage pattern const header = setSearchStr('')} onChange={setSearchStr} onSubmit={null} inputRef={searchInputRef} /> New Channel ; // Empty state if (channels && channels.length === 0) { return <> {header} Create one. >; } // Error state if (channels === undefined) { return <> {header} Could not fetch Channels >; } return <> {header} >; }
The file name format has changed. Click "Reorganize Files" to move existing files to match the new format.