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.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 &&
}
{!create &&
setDownloadModalOpen(false)}>
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
}
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}
}
/>
}
/>
>
}