Refac react frontend (#790)

* Add channel config endpoint

* Add channel aggs

* Add playlist show subscribed only toggle

* Fix refresh always on filterbar toggle

* Add loadingindicator for watchstate change

* Fix missing space in scheduling

* Add schedule request and apprisenotifcation

* Refac upgrade TypeScript target to include 2024 for Object.groupBy

* WIP: Schedule page

* WIP: Schedule page

* Add schedule management ( - notification )

* Fix missing space

* Refac show current selection in input

* Add apprise notifictation url

* Add Stream & Shorts channel pages

* Refac autotarget input on search page

* Fix input requiring 1 instead of 0

* Fix remove unused function

* Chore: npm audit fix

* Refac get channel_overwrites from channelById

* Refac remove defaultvalues form select

* Fix delay content refresh to allow the backend to update subscribed state

* Fix styling selection

* Fix lint

* Fix spelling

* Fix remove unused import

* Chore: update all dependencies - React 19 & vite 6

* Add missing property to ValidatedCookieType

* Refac fix complaints about JSX.Element, used ReactNode instead

* Refac remove unused dependency

* Refac replace react-helmet with react 19 implementation

* Fix Application Settings page

* Chore update dependencies

* Add simple playlist autoplay feature

* Refac use server provided channel images path

* Refac use server provided playlistthumbnail images path

* Add save and restore video playback speed
This commit is contained in:
Merlin
2024-12-22 15:59:30 +01:00
committed by GitHub
parent 75339e479e
commit 5a5d47da9b
54 changed files with 10047 additions and 9526 deletions

View File

@@ -1,462 +1,496 @@
import { Helmet } from 'react-helmet';
import Notifications from '../components/Notifications';
import SettingsNavigation from '../components/SettingsNavigation';
import Button from '../components/Button';
type CronTabType = {
minute: number;
hour: number;
day_of_week: number;
};
type SchedulerErrorType = {
errors: string[];
};
type NotificationItemType = {
task: string;
notification: {
title: string;
urls: string[];
};
};
type SettingsSchedulingResponseType = {
update_subscribed: {
crontab: CronTabType;
};
check_reindex: {
crontab: CronTabType;
task_config: {
days: 0;
};
};
thumbnail_check: {
crontab: CronTabType;
};
download_pending: {
crontab: CronTabType;
};
run_backup: {
crontab: CronTabType;
task_config: {
rotate: false;
};
};
notifications: {
items: NotificationItemType[];
};
scheduler_form: {
update_subscribed: SchedulerErrorType;
download_pending: SchedulerErrorType;
check_reindex: SchedulerErrorType;
thumbnail_check: SchedulerErrorType;
run_backup: SchedulerErrorType;
};
};
const SettingsScheduling = () => {
const response: SettingsSchedulingResponseType = {
update_subscribed: {
crontab: {
minute: 0,
hour: 0,
day_of_week: 0,
},
},
check_reindex: {
crontab: {
minute: 0,
hour: 0,
day_of_week: 0,
},
task_config: {
days: 0,
},
},
thumbnail_check: {
crontab: {
minute: 0,
hour: 0,
day_of_week: 0,
},
},
download_pending: {
crontab: {
minute: 0,
hour: 0,
day_of_week: 0,
},
},
run_backup: {
crontab: {
minute: 0,
hour: 0,
day_of_week: 0,
},
task_config: {
rotate: false,
},
},
notifications: {
items: [
{
task: '',
notification: {
title: '',
urls: [''],
},
},
],
},
scheduler_form: {
update_subscribed: {
errors: ['error?'],
},
download_pending: {
errors: ['error?'],
},
check_reindex: {
errors: ['error?'],
},
thumbnail_check: {
errors: ['error?'],
},
run_backup: {
errors: ['error?'],
},
},
};
const {
check_reindex,
download_pending,
notifications,
run_backup,
scheduler_form,
thumbnail_check,
update_subscribed,
} = response;
return (
<>
<Helmet>
<title>TA | Scheduling Settings</title>
</Helmet>
<div className="boxed-content">
<SettingsNavigation />
<Notifications pageName={'all'} />
<div className="title-bar">
<h1>Scheduler Setup</h1>
<div className="settings-group">
<p>
Schedule settings expect a cron like format, where the first value is minute, second
is hour and third is day of the week.
</p>
<p>Examples:</p>
<ul>
<li>
<span className="settings-current">0 15 *</span>: Run task every day at 15:00 in the
afternoon.
</li>
<li>
<span className="settings-current">30 8 */2</span>: Run task every second day of the
week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
</li>
<li>
<span className="settings-current">auto</span>: Sensible default.
</li>
</ul>
<p>Note:</p>
<ul>
<li>
Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that
reason, the scheduler doesn't support schedules that trigger more than once per
hour.
</li>
</ul>
</div>
</div>
<form action="{% url 'settings_scheduling' %}" method="POST" name="scheduler-update">
<div className="settings-group">
<h2>Rescan Subscriptions</h2>
<div className="settings-item">
<p>
Become a sponsor and join{' '}
<a href="https://members.tubearchivist.com/" target="_blank">
members.tubearchivist.com
</a>{' '}
to get access to <span className="settings-current">real time</span> notifications
for new videos uploaded by your favorite channels.
</p>
<p>
Current rescan schedule:{' '}
<span className="settings-current">
{update_subscribed && (
<>
{update_subscribed.crontab.minute} {update_subscribed.crontab.hour}{' '}
{update_subscribed.crontab.day_of_week}
<Button
label="Delete"
data-schedule="update_subscribed"
onclick="deleteSchedule(this)"
className="danger-button"
/>
</>
)}
{!update_subscribed && 'False'}
</span>
</p>
<p>Periodically rescan your subscriptions:</p>
{scheduler_form.update_subscribed.errors.map(error => {
return (
<p key={error} className="danger-zone">
{error}
</p>
);
})}
<input type="text" name="update_subscribed" id="id_update_subscribed" />
</div>
</div>
<div className="settings-group">
<h2>Start Download</h2>
<div className="settings-item">
<p>
Current Download schedule:{' '}
<span className="settings-current">
{download_pending && (
<>
{download_pending.crontab.minute} {download_pending.crontab.hour}{' '}
{download_pending.crontab.day_of_week}
<Button
label="Delete"
data-schedule="download_pending"
onclick="deleteSchedule(this)"
className="danger-button"
/>
</>
)}
{!download_pending && 'False'}
</span>
</p>
<p>Automatic video download schedule:</p>
{scheduler_form.download_pending.errors.map(error => {
return (
<p key={error} className="danger-zone">
{error}
</p>
);
})}
<input type="text" name="download_pending" id="id_download_pending" />
</div>
</div>
<div className="settings-group">
<h2>Refresh Metadata</h2>
<div className="settings-item">
<p>
Current Metadata refresh schedule:{' '}
<span className="settings-current">
{check_reindex && (
<>
{check_reindex.crontab.minute} {check_reindex.crontab.hour}{' '}
{check_reindex.crontab.day_of_week}
<Button
label="Delete"
data-schedule="check_reindex"
onclick="deleteSchedule(this)"
className="danger-button"
/>
</>
)}
{!check_reindex && 'False'}
</span>
</p>
<p>Daily schedule to refresh metadata from YouTube:</p>
<input type="text" name="check_reindex" id="id_check_reindex" />
</div>
<div className="settings-item">
<p>
Current refresh for metadata older than x days:{' '}
<span className="settings-current">{check_reindex.task_config.days}</span>
</p>
<p>Refresh older than x days, recommended 90:</p>
{scheduler_form.check_reindex.errors.map(error => {
return (
<p key={error} className="danger-zone">
{error}
</p>
);
})}
<input type="number" name="check_reindex_days" id="id_check_reindex_days" />
</div>
</div>
<div className="settings-group">
<h2>Thumbnail Check</h2>
<div className="settings-item">
<p>
Current thumbnail check schedule:{' '}
<span className="settings-current">
{thumbnail_check && (
<>
{thumbnail_check.crontab.minute} {thumbnail_check.crontab.hour}{' '}
{thumbnail_check.crontab.day_of_week}
<Button
label="Delete"
data-schedule="thumbnail_check"
onclick="deleteSchedule(this)"
className="danger-button"
/>
</>
)}
{!thumbnail_check && 'False'}
</span>
</p>
<p>Periodically check and cleanup thumbnails:</p>
{scheduler_form.thumbnail_check.errors.map(error => {
return (
<p key={error} className="danger-zone">
{error}
</p>
);
})}
<input type="text" name="thumbnail_check" id="id_thumbnail_check" />
</div>
</div>
<div className="settings-group">
<h2>ZIP file index backup</h2>
<div className="settings-item">
<p>
<i>
Zip file backups are very slow for large archives and consistency is not
guaranteed, use snapshots instead. Make sure no other tasks are running when
creating a Zip file backup.
</i>
</p>
<p>
Current index backup schedule:{' '}
<span className="settings-current">
{run_backup && (
<>
{run_backup.crontab.minute} {run_backup.crontab.hour}{' '}
{run_backup.crontab.day_of_week}
<Button
label="Delete"
data-schedule="run_backup"
onclick="deleteSchedule(this)"
className="danger-button"
/>
</>
)}
{!run_backup && 'False'}
</span>
</p>
<p>Automatically backup metadata to a zip file:</p>
{scheduler_form.run_backup.errors.map(error => {
return (
<p key={error} className="danger-zone">
{error}
</p>
);
})}
<input type="text" name="run_backup" id="id_run_backup" />
</div>
<div className="settings-item">
<p>
Current backup files to keep:{' '}
<span className="settings-current">{run_backup.task_config.rotate}</span>
</p>
<p>Max auto backups to keep:</p>
<input type="number" name="run_backup_rotate" id="id_run_backup_rotate" />
</div>
</div>
<div className="settings-group">
<h2>Add Notification URL</h2>
<div className="settings-item">
{notifications && (
<>
<p>
<Button
label="Show"
type="button"
onclick="textReveal(this)"
id="text-reveal-button"
/>{' '}
stored notification links
</p>
<div id="text-reveal" className="description-text">
{notifications.items.map(({ task, notification }) => {
return (
<>
<h3 key={task}>{notification.title}</h3>
{notification.urls.map((url: string) => {
return (
<p>
<Button
type="button"
className="danger-button"
label="Delete"
data-url={url}
data-task={task}
onclick="deleteNotificationUrl(this)"
/>
<span> {url}</span>
</p>
);
})}
</>
);
})}
</div>
</>
)}
{!notifications && <p>No notifications stored</p>}
</div>
<div className="settings-item">
<p>
<i>
Send notification on completed tasks with the help of the{' '}
<a href="https://github.com/caronc/apprise" target="_blank">
Apprise
</a>{' '}
library.
</i>
</p>
<select name="task" id="id_task" defaultValue="">
<option value="">-- select task --</option>
<option value="update_subscribed">Rescan your Subscriptions</option>
<option value="extract_download">Add to download queue</option>
<option value="download_pending">Downloading</option>
<option value="check_reindex">Reindex Documents</option>
</select>
<input
type="text"
name="notification_url"
placeholder="Apprise notification URL"
id="id_notification_url"
/>
</div>
</div>
<Button type="submit" name="scheduler-settings" label="Update Scheduler Settings" />
</form>
</div>
</>
);
};
export default SettingsScheduling;
import Notifications from '../components/Notifications';
import SettingsNavigation from '../components/SettingsNavigation';
import Button from '../components/Button';
import PaginationDummy from '../components/PaginationDummy';
import { useEffect, useState } from 'react';
import loadSchedule, { ScheduleResponseType } from '../api/loader/loadSchedule';
import loadAppriseNotification, {
AppriseNotificationType,
} from '../api/loader/loadAppriseNotification';
import deleteTaskSchedule from '../api/actions/deleteTaskSchedule';
import createTaskSchedule from '../api/actions/createTaskSchedule';
import createAppriseNotificationUrl, {
AppriseTaskNameType,
} from '../api/actions/createAppriseNotificationUrl';
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
const SettingsScheduling = () => {
const [refresh, setRefresh] = useState(false);
const [scheduleResponse, setScheduleResponse] = useState<ScheduleResponseType>([]);
const [appriseNotification, setAppriseNotification] = useState<AppriseNotificationType>();
const [updateSubscribed, setUpdateSubscribed] = useState<string | undefined>();
const [downloadPending, setDownloadPending] = useState<string | undefined>();
const [checkReindex, setCheckReindex] = useState<string | undefined>();
const [checkReindexDays, setCheckReindexDays] = useState<number | undefined>(undefined);
const [thumbnailCheck, setThumbnailCheck] = useState<string | undefined>();
const [zipBackup, setZipBackup] = useState<string | undefined>();
const [zipBackupDays, setZipBackupDays] = useState<number | undefined>(undefined);
const [notificationUrl, setNotificationUrl] = useState<string | undefined>(undefined);
const [notificationTask, setNotificationTask] = useState<AppriseTaskNameType | string>('');
useEffect(() => {
(async () => {
if (refresh) {
const scheduleResponse = await loadSchedule();
const appriseNotificationResponse = await loadAppriseNotification();
setScheduleResponse(scheduleResponse);
setAppriseNotification(appriseNotificationResponse);
setRefresh(false);
}
})();
}, [refresh]);
useEffect(() => {
setRefresh(true);
}, []);
const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
console.log(groupedSchedules);
const { update_subscribed, download_pending, run_backup, check_reindex, thumbnail_check } =
groupedSchedules;
const updateSubscribedSchedule = update_subscribed?.pop();
const downloadPendingSchedule = download_pending?.pop();
const runBackup = run_backup?.pop();
const checkReindexSchedule = check_reindex?.pop();
const thumbnailCheckSchedule = thumbnail_check?.pop();
return (
<>
<title>TA | Scheduling Settings</title>
<div className="boxed-content">
<SettingsNavigation />
<Notifications pageName={'all'} />
<div className="title-bar">
<h1>Scheduler Setup</h1>
<div className="settings-group">
<p>
Schedule settings expect a cron like format, where the first value is minute, second
is hour and third is day of the week.
</p>
<p>Examples:</p>
<ul>
<li>
<span className="settings-current">0 15 *</span>: Run task every day at 15:00 in the
afternoon.
</li>
<li>
<span className="settings-current">30 8 */2</span>: Run task every second day of the
week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
</li>
<li>
<span className="settings-current">auto</span>: Sensible default.
</li>
</ul>
<p>Note:</p>
<ul>
<li>
Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that
reason, the scheduler doesn't support schedules that trigger more than once per
hour.
</li>
</ul>
</div>
</div>
<div className="settings-group">
<h2>Rescan Subscriptions</h2>
<div className="settings-item">
<p>
Become a sponsor and join{' '}
<a href="https://members.tubearchivist.com/" target="_blank">
members.tubearchivist.com
</a>{' '}
to get access to <span className="settings-current">real time</span> notifications for
new videos uploaded by your favorite channels.
</p>
<p>
Current rescan schedule:{' '}
<span className="settings-current">
{!updateSubscribedSchedule && 'False'}
{updateSubscribedSchedule && (
<>
{updateSubscribedSchedule?.schedule}{' '}
<Button
label="Delete"
data-schedule="update_subscribed"
onClick={async () => {
await deleteTaskSchedule('update_subscribed');
setRefresh(true);
}}
className="danger-button"
/>
</>
)}
</span>
</p>
<p>Periodically rescan your subscriptions:</p>
<input
type="text"
value={updateSubscribed || updateSubscribedSchedule?.schedule}
onChange={e => {
setUpdateSubscribed(e.currentTarget.value);
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('update_subscribed', {
schedule: updateSubscribed,
});
setUpdateSubscribed('');
setRefresh(true);
}}
/>
</div>
</div>
<div className="settings-group">
<h2>Start Download</h2>
<div className="settings-item">
<p>
Current Download schedule:{' '}
<span className="settings-current">
{!download_pending && 'False'}
{downloadPendingSchedule && (
<>
{downloadPendingSchedule?.schedule}{' '}
<Button
label="Delete"
className="danger-button"
onClick={async () => {
await deleteTaskSchedule('download_pending');
setRefresh(true);
}}
/>
</>
)}
</span>
</p>
<p>Automatic video download schedule:</p>
<input
type="text"
value={downloadPending || downloadPendingSchedule?.schedule}
onChange={e => {
setDownloadPending(e.currentTarget.value);
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('download_pending', {
schedule: downloadPending,
});
setDownloadPending('');
setRefresh(true);
}}
/>
</div>
</div>
<div className="settings-group">
<h2>Refresh Metadata</h2>
<div className="settings-item">
<p>
Current Metadata refresh schedule:{' '}
<span className="settings-current">
{!checkReindexSchedule && 'False'}
{checkReindexSchedule && (
<>
{checkReindexSchedule?.schedule}{' '}
<Button
label="Delete"
className="danger-button"
onClick={async () => {
await deleteTaskSchedule('check_reindex');
setRefresh(true);
}}
/>
</>
)}
</span>
</p>
<p>Daily schedule to refresh metadata from YouTube:</p>
<input
type="text"
value={checkReindex || checkReindexSchedule?.schedule}
onChange={e => {
setCheckReindex(e.currentTarget.value);
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('check_reindex', {
schedule: checkReindex,
});
setCheckReindex('');
setRefresh(true);
}}
/>
</div>
<div className="settings-item">
<p>
Current refresh for metadata older than x days:{' '}
<span className="settings-current">{checkReindexSchedule?.config?.days}</span>
</p>
<p>Refresh older than x days, recommended 90:</p>
<input
type="number"
value={checkReindexDays || checkReindexSchedule?.config?.days}
onChange={e => {
setCheckReindexDays(Number(e.currentTarget.value));
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('check_reindex', {
config: {
days: checkReindexDays,
},
});
setCheckReindexDays(undefined);
setRefresh(true);
}}
/>
</div>
</div>
<div className="settings-group">
<h2>Thumbnail Check</h2>
<div className="settings-item">
<p>
Current thumbnail check schedule:{' '}
<span className="settings-current">
{!thumbnailCheckSchedule && 'False'}
{thumbnailCheckSchedule && (
<>
{thumbnailCheckSchedule?.schedule}{' '}
<Button
label="Delete"
className="danger-button"
onClick={async () => {
await deleteTaskSchedule('thumbnail_check');
setRefresh(true);
}}
/>
</>
)}
</span>
</p>
<p>Periodically check and cleanup thumbnails:</p>
<input
type="text"
value={thumbnailCheck || thumbnailCheckSchedule?.schedule}
onChange={e => {
setThumbnailCheck(e.currentTarget.value);
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('thumbnail_check', {
schedule: thumbnailCheck,
});
setThumbnailCheck('');
setRefresh(true);
}}
/>
</div>
</div>
<div className="settings-group">
<h2>ZIP file index backup</h2>
<div className="settings-item">
<p>
<i>
Zip file backups are very slow for large archives and consistency is not guaranteed,
use snapshots instead. Make sure no other tasks are running when creating a Zip file
backup.
</i>
</p>
<p>
Current index backup schedule:{' '}
<span className="settings-current">
{!runBackup && 'False'}
{runBackup && (
<>
{runBackup.schedule}{' '}
<Button
label="Delete"
className="danger-button"
onClick={async () => {
await deleteTaskSchedule('run_backup');
setRefresh(true);
}}
/>
</>
)}
</span>
</p>
<p>Automatically backup metadata to a zip file:</p>
<input
type="text"
value={zipBackup || runBackup?.schedule}
onChange={e => {
setZipBackup(e.currentTarget.value);
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('run_backup', {
schedule: zipBackup,
});
setZipBackup('');
setRefresh(true);
}}
/>
</div>
<div className="settings-item">
<p>
Current backup files to keep:{' '}
<span className="settings-current">{runBackup?.config?.rotate}</span>
</p>
<p>Max auto backups to keep:</p>
<input
type="number"
value={(zipBackupDays || runBackup?.config?.rotate)?.toString()}
onChange={e => {
setZipBackupDays(Number(e.currentTarget.value));
}}
/>
<Button
label="Save"
onClick={async () => {
await createTaskSchedule('run_backup', {
config: {
rotate: zipBackupDays,
},
});
setZipBackupDays(undefined);
setRefresh(true);
}}
/>
</div>
</div>
<div className="settings-group">
<h2>Add Notification URL</h2>
<div className="settings-item">
{!appriseNotification && <p>No notifications stored</p>}
{appriseNotification && (
<>
<div className="description-text">
{Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
return (
<>
<h3 key={key}>{title}</h3>
{urls.map((url: string) => {
return (
<p>
<span>{url} </span>
<Button
type="button"
className="danger-button"
label="Delete"
onClick={async () => {
await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
setRefresh(true);
}}
/>
</p>
);
})}
</>
);
})}
</div>
</>
)}
</div>
<div className="settings-item">
<p>
<i>
Send notification on completed tasks with the help of the{' '}
<a href="https://github.com/caronc/apprise" target="_blank">
Apprise
</a>{' '}
library.
</i>
</p>
<select
defaultValue=""
value={notificationTask}
onChange={e => {
setNotificationTask(e.currentTarget.value);
}}
>
<option value="">-- select task --</option>
<option value="update_subscribed">Rescan your Subscriptions</option>
<option value="extract_download">Add to download queue</option>
<option value="download_pending">Downloading</option>
<option value="check_reindex">Reindex Documents</option>
</select>
<input
type="text"
placeholder="Apprise notification URL"
value={notificationUrl}
onChange={e => {
setNotificationUrl(e.currentTarget.value);
}}
/>
<Button
label="Save"
onClick={async () => {
await createAppriseNotificationUrl(
notificationTask as AppriseTaskNameType,
notificationUrl || '',
);
setRefresh(true);
}}
/>
</div>
</div>
<PaginationDummy />
</div>
</>
);
};
export default SettingsScheduling;