No channels found.
;
- }
-
- return (
- <>
- {channelList.map(channel => {
- return (
- No channels found.
;
+ }
+
+ return (
+ <>
+ {channelList.map(channel => {
+ return (
+
@@ -35,7 +30,7 @@ const Filterbar = ({
checked={userConfig.config.hide_watched}
onChange={() => {
setRefresh?.(true);
- setPartialConfig({hide_watched: !userConfig.config.hide_watched})
+ setPartialConfig({ hide_watched: !userConfig.config.hide_watched });
}}
/>
@@ -48,7 +43,6 @@ const Filterbar = ({
Off
)}
-
@@ -62,7 +56,7 @@ const Filterbar = ({
value={userConfig.config.sort_by}
onChange={event => {
setRefresh?.(true);
- setPartialConfig({sort_by: event.target.value as SortByType});
+ setPartialConfig({ sort_by: event.target.value as SortByType });
}}
>
diff --git a/frontend/src/components/GoogleCast.tsx b/frontend/src/components/GoogleCast.tsx
index 8d7618dc..f75175e4 100644
--- a/frontend/src/components/GoogleCast.tsx
+++ b/frontend/src/components/GoogleCast.tsx
@@ -1,226 +1,226 @@
-import { useCallback, useEffect, useState } from 'react';
-import { VideoType } from '../pages/Home';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import updateVideoProgressById from '../api/actions/updateVideoProgressById';
-import watchedThreshold from '../functions/watchedThreshold';
-
-const getURL = () => {
- return window.location.origin;
-};
-
-function shiftCurrentTime(contentCurrentTime: number | undefined) {
- console.log(contentCurrentTime);
- if (contentCurrentTime === undefined) {
- return 0;
- }
-
- // Shift media back 3 seconds to prevent missing some of the content
- if (contentCurrentTime > 5) {
- return contentCurrentTime - 3;
- } else {
- return 0;
- }
-}
-
-async function castVideoProgress(
- player: {
- mediaInfo: { contentId: string | string[] };
- currentTime: number;
- duration: number;
- },
- video: VideoType | undefined,
-) {
- if (!video) {
- console.log('castVideoProgress: Video to cast not found...');
- return;
- }
- const videoId = video.youtube_id;
-
- if (player.mediaInfo.contentId.includes(videoId)) {
- const currentTime = player.currentTime;
- const duration = player.duration;
-
- if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
- // Check progress every 10 seconds or else progress is checked a few times a second
- await updateVideoProgressById({
- youtubeId: videoId,
- currentProgress: currentTime,
- });
-
- if (!video.player.watched) {
- // Check if video is already marked as watched
- if (watchedThreshold(currentTime, duration)) {
- await updateWatchedState({
- id: videoId,
- is_watched: true,
- });
- }
- }
- }
- }
-}
-
-async function castVideoPaused(
- player: {
- currentTime: number;
- duration: number;
- mediaInfo: { contentId: string | string[] } | null;
- },
- video: VideoType | undefined,
-) {
- if (!video) {
- console.log('castVideoPaused: Video to cast not found...');
- return;
- }
-
- const videoId = video?.youtube_id;
-
- const currentTime = player.currentTime;
- const duration = player.duration;
-
- if (player.mediaInfo != null) {
- if (player.mediaInfo.contentId.includes(videoId)) {
- if (currentTime !== 0 && duration !== 0) {
- await updateVideoProgressById({
- youtubeId: videoId,
- currentProgress: currentTime,
- });
- }
- }
- }
-}
-
-type GoogleCastProps = {
- video?: VideoType;
- setRefresh?: () => void;
-};
-
-const GoogleCast = ({ video, setRefresh }: GoogleCastProps) => {
- const [isConnected, setIsConnected] = useState(false);
-
- const setup = useCallback(() => {
- const cast = globalThis.cast;
- const chrome = globalThis.chrome;
-
- cast.framework.CastContext.getInstance().setOptions({
- receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
- autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
- });
-
- const player = new cast.framework.RemotePlayer();
-
- const playerController = new cast.framework.RemotePlayerController(player);
-
- // Add event listerner to check if a connection to a cast device is initiated
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
- function () {
- setIsConnected(player.isConnected);
- },
- );
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
- function () {
- castVideoProgress(player, video);
- },
- );
- playerController.addEventListener(
- cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
- function () {
- castVideoPaused(player, video);
- setRefresh?.();
- },
- );
- }, [setRefresh, video]);
-
- const startPlayback = useCallback(() => {
- const chrome = globalThis.chrome;
- const cast = globalThis.cast;
- const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
-
- const mediaUrl = video?.media_url;
- const vidThumbUrl = video?.vid_thumb_url;
- const contentTitle = video?.title;
- const contentId = `${getURL()}${mediaUrl}`;
- const contentImage = `${getURL()}${vidThumbUrl}`;
- const contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
-
- const contentSubtitles = [];
- const videoSubtitles = video?.subtitles; // Array of subtitles
- if (typeof videoSubtitles !== 'undefined') {
- for (let i = 0; i < videoSubtitles.length; i++) {
- const subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
-
- subtitle.trackContentId = videoSubtitles[i].media_url;
- subtitle.trackContentType = 'text/vtt';
- subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
- subtitle.name = videoSubtitles[i].name;
- subtitle.language = videoSubtitles[i].lang;
- subtitle.customData = null;
-
- contentSubtitles.push(subtitle);
- }
- }
-
- const mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
- // mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
- mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
- mediaInfo.metadata.title = contentTitle?.replace('&', '&'); // Set the video title
- mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
- // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
- mediaInfo.tracks = contentSubtitles;
-
- const request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
- // request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
- request.currentTime = shiftCurrentTime(video?.player?.position); // Set video start position based on the browser video position
- // request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
-
- castSession.loadMedia(request).then(
- function () {
- console.log('media loaded');
- },
- function (error: { code: string }) {
- console.log('Error', error, 'Error code: ' + error.code);
- },
- ); // Send request to cast device
-
- // Do not add videoProgress?.position, this will cause loops!
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [video?.media_url, video?.subtitles, video?.title, video?.vid_thumb_url]);
-
- useEffect(() => {
- // @ts-expect-error __onGCastApiAvailable is the google cast window hook ( source: https://developers.google.com/cast/docs/web_sender/integrate )
- window['__onGCastApiAvailable'] = function (isAvailable: boolean) {
- if (isAvailable) {
- setup();
- }
- };
- }, [setup]);
-
- useEffect(() => {
- console.log('isConnected', isConnected);
- if (isConnected) {
- startPlayback();
- }
- }, [isConnected, startPlayback]);
-
- if (!video) {
- return
Video for cast not found...
;
- }
-
- return (
- <>
- <>
-
-
- {/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
-
- >
- >
- );
-};
-
-export default GoogleCast;
+import { useCallback, useEffect, useState } from 'react';
+import { VideoType } from '../pages/Home';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import updateVideoProgressById from '../api/actions/updateVideoProgressById';
+import watchedThreshold from '../functions/watchedThreshold';
+
+const getURL = () => {
+ return window.location.origin;
+};
+
+function shiftCurrentTime(contentCurrentTime: number | undefined) {
+ console.log(contentCurrentTime);
+ if (contentCurrentTime === undefined) {
+ return 0;
+ }
+
+ // Shift media back 3 seconds to prevent missing some of the content
+ if (contentCurrentTime > 5) {
+ return contentCurrentTime - 3;
+ } else {
+ return 0;
+ }
+}
+
+async function castVideoProgress(
+ player: {
+ mediaInfo: { contentId: string | string[] };
+ currentTime: number;
+ duration: number;
+ },
+ video: VideoType | undefined,
+) {
+ if (!video) {
+ console.log('castVideoProgress: Video to cast not found...');
+ return;
+ }
+ const videoId = video.youtube_id;
+
+ if (player.mediaInfo.contentId.includes(videoId)) {
+ const currentTime = player.currentTime;
+ const duration = player.duration;
+
+ if (currentTime % 10 <= 1.0 && currentTime !== 0 && duration !== 0) {
+ // Check progress every 10 seconds or else progress is checked a few times a second
+ await updateVideoProgressById({
+ youtubeId: videoId,
+ currentProgress: currentTime,
+ });
+
+ if (!video.player.watched) {
+ // Check if video is already marked as watched
+ if (watchedThreshold(currentTime, duration)) {
+ await updateWatchedState({
+ id: videoId,
+ is_watched: true,
+ });
+ }
+ }
+ }
+ }
+}
+
+async function castVideoPaused(
+ player: {
+ currentTime: number;
+ duration: number;
+ mediaInfo: { contentId: string | string[] } | null;
+ },
+ video: VideoType | undefined,
+) {
+ if (!video) {
+ console.log('castVideoPaused: Video to cast not found...');
+ return;
+ }
+
+ const videoId = video?.youtube_id;
+
+ const currentTime = player.currentTime;
+ const duration = player.duration;
+
+ if (player.mediaInfo != null) {
+ if (player.mediaInfo.contentId.includes(videoId)) {
+ if (currentTime !== 0 && duration !== 0) {
+ await updateVideoProgressById({
+ youtubeId: videoId,
+ currentProgress: currentTime,
+ });
+ }
+ }
+ }
+}
+
+type GoogleCastProps = {
+ video?: VideoType;
+ setRefresh?: () => void;
+};
+
+const GoogleCast = ({ video, setRefresh }: GoogleCastProps) => {
+ const [isConnected, setIsConnected] = useState(false);
+
+ const setup = useCallback(() => {
+ const cast = globalThis.cast;
+ const chrome = globalThis.chrome;
+
+ cast.framework.CastContext.getInstance().setOptions({
+ receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, // Use built in receiver app on cast device, see https://developers.google.com/cast/docs/styled_receiver if you want to be able to add a theme, splash screen or watermark. Has a $5 one time fee.
+ autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
+ });
+
+ const player = new cast.framework.RemotePlayer();
+
+ const playerController = new cast.framework.RemotePlayerController(player);
+
+ // Add event listerner to check if a connection to a cast device is initiated
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
+ function () {
+ setIsConnected(player.isConnected);
+ },
+ );
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED,
+ function () {
+ castVideoProgress(player, video);
+ },
+ );
+ playerController.addEventListener(
+ cast.framework.RemotePlayerEventType.IS_PAUSED_CHANGED,
+ function () {
+ castVideoPaused(player, video);
+ setRefresh?.();
+ },
+ );
+ }, [setRefresh, video]);
+
+ const startPlayback = useCallback(() => {
+ const chrome = globalThis.chrome;
+ const cast = globalThis.cast;
+ const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
+
+ const mediaUrl = video?.media_url;
+ const vidThumbUrl = video?.vid_thumb_url;
+ const contentTitle = video?.title;
+ const contentId = `${getURL()}${mediaUrl}`;
+ const contentImage = `${getURL()}${vidThumbUrl}`;
+ const contentType = 'video/mp4'; // Set content type, only videos right now so it is hard coded
+
+ const contentSubtitles = [];
+ const videoSubtitles = video?.subtitles; // Array of subtitles
+ if (typeof videoSubtitles !== 'undefined') {
+ for (let i = 0; i < videoSubtitles.length; i++) {
+ const subtitle = new chrome.cast.media.Track(i, chrome.cast.media.TrackType.TEXT);
+
+ subtitle.trackContentId = videoSubtitles[i].media_url;
+ subtitle.trackContentType = 'text/vtt';
+ subtitle.subtype = chrome.cast.media.TextTrackType.SUBTITLES;
+ subtitle.name = videoSubtitles[i].name;
+ subtitle.language = videoSubtitles[i].lang;
+ subtitle.customData = null;
+
+ contentSubtitles.push(subtitle);
+ }
+ }
+
+ const mediaInfo = new chrome.cast.media.MediaInfo(contentId, contentType); // Create MediaInfo var that contains url and content type
+ // mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED; // Set type of stream, BUFFERED, LIVE, OTHER
+ mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); // Create metadata var and add it to MediaInfo
+ mediaInfo.metadata.title = contentTitle?.replace('&', '&'); // Set the video title
+ mediaInfo.metadata.images = [new chrome.cast.Image(contentImage)]; // Set the video thumbnail
+ // mediaInfo.textTrackStyle = new chrome.cast.media.TextTrackStyle();
+ mediaInfo.tracks = contentSubtitles;
+
+ const request = new chrome.cast.media.LoadRequest(mediaInfo); // Create request with the previously set MediaInfo.
+ // request.queueData = new chrome.cast.media.QueueData(); // See https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData for playlist support.
+ request.currentTime = shiftCurrentTime(video?.player?.position); // Set video start position based on the browser video position
+ // request.activeTrackIds = contentActiveSubtitle; // Set active subtitle based on video player
+
+ castSession.loadMedia(request).then(
+ function () {
+ console.log('media loaded');
+ },
+ function (error: { code: string }) {
+ console.log('Error', error, 'Error code: ' + error.code);
+ },
+ ); // Send request to cast device
+
+ // Do not add videoProgress?.position, this will cause loops!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [video?.media_url, video?.subtitles, video?.title, video?.vid_thumb_url]);
+
+ useEffect(() => {
+ // @ts-expect-error __onGCastApiAvailable is the google cast window hook ( source: https://developers.google.com/cast/docs/web_sender/integrate )
+ window['__onGCastApiAvailable'] = function (isAvailable: boolean) {
+ if (isAvailable) {
+ setup();
+ }
+ };
+ }, [setup]);
+
+ useEffect(() => {
+ console.log('isConnected', isConnected);
+ if (isConnected) {
+ startPlayback();
+ }
+ }, [isConnected, startPlayback]);
+
+ if (!video) {
+ return
Video for cast not found...
;
+ }
+
+ return (
+ <>
+ <>
+
+
+ {/* @ts-expect-error React does not know what to do with the google-cast-launcher, but it works. */}
+
+ >
+ >
+ );
+};
+
+export default GoogleCast;
diff --git a/frontend/src/components/Navigation.tsx b/frontend/src/components/Navigation.tsx
index ad0d923c..13232442 100644
--- a/frontend/src/components/Navigation.tsx
+++ b/frontend/src/components/Navigation.tsx
@@ -8,7 +8,6 @@ import logOut from '../api/actions/logOut';
import loadIsAdmin from '../functions/getIsAdmin';
const Navigation = () => {
-
const isAdmin = loadIsAdmin();
const navigate = useNavigate();
const handleLogout = async (event: { preventDefault: () => void }) => {
diff --git a/frontend/src/components/Notifications.tsx b/frontend/src/components/Notifications.tsx
index 33eb58fa..bfc107da 100644
--- a/frontend/src/components/Notifications.tsx
+++ b/frontend/src/components/Notifications.tsx
@@ -1,101 +1,101 @@
-import { Fragment, useEffect, useState } from 'react';
-import loadNotifications, { NotificationPages } from '../api/loader/loadNotifications';
-import iconStop from '/img/icon-stop.svg';
-import stopTaskByName from '../api/actions/stopTaskByName';
-
-type NotificationType = {
- title: string;
- group: string;
- api_stop: boolean;
- level: string;
- id: string;
- command: boolean | string;
- messages: string[];
- progress: number;
-};
-
-type NotificationResponseType = NotificationType[];
-
-type NotificationsProps = {
- pageName: NotificationPages;
- includeReindex?: boolean;
- update?: boolean;
- setShouldRefresh?: (isDone: boolean) => void;
-};
-
-const Notifications = ({
- pageName,
- includeReindex = false,
- update,
- setShouldRefresh,
-}: NotificationsProps) => {
- const [notificationResponse, setNotificationResponse] = useState
([]);
-
- useEffect(() => {
- const intervalId = setInterval(async () => {
- const notifications = await loadNotifications(pageName, includeReindex);
-
- if (notifications.length === 0) {
- setNotificationResponse(notifications);
- clearInterval(intervalId);
- setShouldRefresh?.(true);
- return;
- } else {
- setShouldRefresh?.(false);
- }
-
- setNotificationResponse(notifications);
- }, 500);
-
- return () => {
- clearInterval(intervalId);
- };
- }, [pageName, update, setShouldRefresh, includeReindex]);
-
- if (notificationResponse.length === 0) {
- return [];
- }
-
- return (
- <>
- {notificationResponse.map(notification => (
-
-
{notification.title}
-
- {notification.messages.map?.(message => {
- return (
-
- {message}
-
-
- );
- }) || notification.messages}
-
-
- {notification['api_stop'] && notification.command !== 'STOP' && (
-
{
- await stopTaskByName(notification.id);
- }}
- />
- )}
-
-
-
- ))}
- >
- );
-};
-
-export default Notifications;
+import { Fragment, useEffect, useState } from 'react';
+import loadNotifications, { NotificationPages } from '../api/loader/loadNotifications';
+import iconStop from '/img/icon-stop.svg';
+import stopTaskByName from '../api/actions/stopTaskByName';
+
+type NotificationType = {
+ title: string;
+ group: string;
+ api_stop: boolean;
+ level: string;
+ id: string;
+ command: boolean | string;
+ messages: string[];
+ progress: number;
+};
+
+type NotificationResponseType = NotificationType[];
+
+type NotificationsProps = {
+ pageName: NotificationPages;
+ includeReindex?: boolean;
+ update?: boolean;
+ setShouldRefresh?: (isDone: boolean) => void;
+};
+
+const Notifications = ({
+ pageName,
+ includeReindex = false,
+ update,
+ setShouldRefresh,
+}: NotificationsProps) => {
+ const [notificationResponse, setNotificationResponse] = useState([]);
+
+ useEffect(() => {
+ const intervalId = setInterval(async () => {
+ const notifications = await loadNotifications(pageName, includeReindex);
+
+ if (notifications.length === 0) {
+ setNotificationResponse(notifications);
+ clearInterval(intervalId);
+ setShouldRefresh?.(true);
+ return;
+ } else {
+ setShouldRefresh?.(false);
+ }
+
+ setNotificationResponse(notifications);
+ }, 500);
+
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, [pageName, update, setShouldRefresh, includeReindex]);
+
+ if (notificationResponse.length === 0) {
+ return [];
+ }
+
+ return (
+ <>
+ {notificationResponse.map(notification => (
+
+
{notification.title}
+
+ {notification.messages.map?.(message => {
+ return (
+
+ {message}
+
+
+ );
+ }) || notification.messages}
+
+
+ {notification['api_stop'] && notification.command !== 'STOP' && (
+
{
+ await stopTaskByName(notification.id);
+ }}
+ />
+ )}
+
+
+
+ ))}
+ >
+ );
+};
+
+export default Notifications;
diff --git a/frontend/src/components/PaginationDummy.tsx b/frontend/src/components/PaginationDummy.tsx
index e6f80b13..bea1c0c9 100644
--- a/frontend/src/components/PaginationDummy.tsx
+++ b/frontend/src/components/PaginationDummy.tsx
@@ -1,9 +1,9 @@
-const PaginationDummy = () => {
- return (
-
-
{/** dummy pagination for consistent padding */}
-
- );
-};
-
-export default PaginationDummy;
+const PaginationDummy = () => {
+ return (
+
+
{/** dummy pagination for consistent padding */}
+
+ );
+};
+
+export default PaginationDummy;
diff --git a/frontend/src/components/PlaylistList.tsx b/frontend/src/components/PlaylistList.tsx
index 88164144..a0cbd0bf 100644
--- a/frontend/src/components/PlaylistList.tsx
+++ b/frontend/src/components/PlaylistList.tsx
@@ -1,90 +1,89 @@
-import { Link } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import { PlaylistType } from '../pages/Playlist';
-import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
-import formatDate from '../functions/formatDates';
-import Button from './Button';
-import PlaylistThumbnail from './PlaylistThumbnail';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-type PlaylistListProps = {
- playlistList: PlaylistType[] | undefined;
- setRefresh: (status: boolean) => void;
-};
-
-const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
-
- const { userConfig } = useUserConfigStore();
- const viewLayout = userConfig.config.view_style_playlist;
-
- if (!playlistList || playlistList.length === 0) {
- return No playlists found.
;
- }
-
- return (
- <>
- {playlistList.map((playlist: PlaylistType) => {
- return (
-
-
-
- {playlist.playlist_type != 'custom' && (
-
-
{playlist.playlist_channel}
-
- )}
-
-
-
{playlist.playlist_name}
-
-
-
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
-
- {playlist.playlist_type != 'custom' && (
- <>
- {playlist.playlist_subscribed && (
-
{
- await updatePlaylistSubscription(playlist.playlist_id, false);
-
- setRefresh(true);
- }}
- />
- )}
-
- {!playlist.playlist_subscribed && (
- {
- await updatePlaylistSubscription(playlist.playlist_id, true);
-
- setTimeout(() => {
- setRefresh(true);
- }, 500);
- }}
- />
- )}
- >
- )}
-
-
- );
- })}
- >
- );
-};
-
-export default PlaylistList;
+import { Link } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import { PlaylistType } from '../pages/Playlist';
+import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
+import formatDate from '../functions/formatDates';
+import Button from './Button';
+import PlaylistThumbnail from './PlaylistThumbnail';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+type PlaylistListProps = {
+ playlistList: PlaylistType[] | undefined;
+ setRefresh: (status: boolean) => void;
+};
+
+const PlaylistList = ({ playlistList, setRefresh }: PlaylistListProps) => {
+ const { userConfig } = useUserConfigStore();
+ const viewLayout = userConfig.config.view_style_playlist;
+
+ if (!playlistList || playlistList.length === 0) {
+ return No playlists found.
;
+ }
+
+ return (
+ <>
+ {playlistList.map((playlist: PlaylistType) => {
+ return (
+
+
+
+ {playlist.playlist_type != 'custom' && (
+
+
{playlist.playlist_channel}
+
+ )}
+
+
+
{playlist.playlist_name}
+
+
+
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
+
+ {playlist.playlist_type != 'custom' && (
+ <>
+ {playlist.playlist_subscribed && (
+
{
+ await updatePlaylistSubscription(playlist.playlist_id, false);
+
+ setRefresh(true);
+ }}
+ />
+ )}
+
+ {!playlist.playlist_subscribed && (
+ {
+ await updatePlaylistSubscription(playlist.playlist_id, true);
+
+ setTimeout(() => {
+ setRefresh(true);
+ }, 500);
+ }}
+ />
+ )}
+ >
+ )}
+
+
+ );
+ })}
+ >
+ );
+};
+
+export default PlaylistList;
diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx
index 580d3b36..0e1c79e2 100644
--- a/frontend/src/components/VideoPlayer.tsx
+++ b/frontend/src/components/VideoPlayer.tsx
@@ -1,262 +1,262 @@
-import updateVideoProgressById from '../api/actions/updateVideoProgressById';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
-import watchedThreshold from '../functions/watchedThreshold';
-import Notifications from './Notifications';
-import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useState } from 'react';
-import formatTime from '../functions/formatTime';
-import { useSearchParams } from 'react-router-dom';
-import getApiUrl from '../configuration/getApiUrl';
-
-type VideoTag = SyntheticEvent;
-
-export type SkippedSegmentType = {
- from: number;
- to: number;
-};
-
-export type SponsorSegmentsSkippedType = Record;
-
-type Subtitle = {
- name: string;
- source: string;
- lang: string;
- media_url: string;
-};
-
-type SubtitlesProp = {
- subtitles: Subtitle[];
-};
-
-const Subtitles = ({ subtitles }: SubtitlesProp) => {
- return subtitles.map((subtitle: Subtitle) => {
- let label = subtitle.name;
-
- if (subtitle.source === 'auto') {
- label += ' - auto';
- }
-
- return (
-
- );
- });
-};
-
-const handleTimeUpdate =
- (
- youtubeId: string,
- duration: number,
- watched: boolean,
- sponsorBlock?: SponsorBlockType,
- setSponsorSegmentSkipped?: Dispatch>,
- ) =>
- async (videoTag: VideoTag) => {
- const currentTime = Number(videoTag.currentTarget.currentTime);
-
- if (sponsorBlock && sponsorBlock.segments) {
- sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
- const [from, to] = segment.segment;
-
- if (currentTime >= from && currentTime <= from + 0.3) {
- videoTag.currentTarget.currentTime = to;
-
- setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
- return { ...segments, [segment.UUID]: { from, to } };
- });
- }
-
- if (currentTime > to + 10) {
- setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
- return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
- });
- }
- });
- }
-
- if (currentTime < 10) return;
- if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
- // Check progress every 10 seconds or else progress is checked a few times a second
- await updateVideoProgressById({
- youtubeId,
- currentProgress: currentTime,
- });
-
- if (!watched) {
- // Check if video is already marked as watched
- if (watchedThreshold(currentTime, duration)) {
- await updateWatchedState({
- id: youtubeId,
- is_watched: true,
- });
- }
- }
- }
- };
-
-type VideoPlayerProps = {
- video: VideoResponseType;
- sponsorBlock?: SponsorBlockType;
- embed?: boolean;
- autoplay?: boolean;
- onVideoEnd?: () => void;
-};
-
-const VideoPlayer = ({
- video,
- sponsorBlock,
- embed,
- autoplay = false,
- onVideoEnd,
-}: VideoPlayerProps) => {
- const [searchParams] = useSearchParams();
- const searchParamVideoProgress = searchParams.get('t');
-
- const [skippedSegments, setSkippedSegments] = useState({});
-
- const videoId = video.data.youtube_id;
- const videoUrl = video.data.media_url;
- const videoThumbUrl = video.data.vid_thumb_url;
- const watched = video.data.player.watched;
- const duration = video.data.player.duration;
- const videoSubtitles = video.data.subtitles;
-
- let videoSrcProgress =
- Number(video.data.player?.position) > 0 ? Number(video.data.player?.position) : '';
-
- if (searchParamVideoProgress !== null) {
- videoSrcProgress = searchParamVideoProgress;
- }
-
- const handleVideoEnd =
- (
- youtubeId: string,
- watched: boolean,
- setSponsorSegmentSkipped?: Dispatch>,
- ) =>
- async () => {
- if (!watched) {
- // Check if video is already marked as watched
- await updateWatchedState({ id: youtubeId, is_watched: true });
- }
-
- setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
- const keys = Object.keys(segments);
-
- keys.forEach(uuid => {
- segments[uuid] = { from: 0, to: 0 };
- });
-
- return segments;
- });
-
- onVideoEnd?.();
- };
-
- return (
- <>
-
-
- {
- localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
- }}
- onRateChange={(videoTag: VideoTag) => {
- localStorage.setItem('playerSpeed', videoTag.currentTarget.playbackRate.toString());
- }}
- onLoadStart={(videoTag: VideoTag) => {
- videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume') ?? 1);
- videoTag.currentTarget.playbackRate = Number(
- localStorage.getItem('playerSpeed') ?? 1,
- );
- }}
- onTimeUpdate={handleTimeUpdate(
- videoId,
- duration,
- watched,
- sponsorBlock,
- setSkippedSegments,
- )}
- onPause={async (videoTag: VideoTag) => {
- const currentTime = Number(videoTag.currentTarget.currentTime);
-
- if (currentTime < 10) return;
-
- await updateVideoProgressById({
- youtubeId: videoId,
- currentProgress: currentTime,
- });
- }}
- onEnded={handleVideoEnd(videoId, watched)}
- autoPlay={autoplay}
- controls
- width="100%"
- playsInline
- id="video-item"
- >
-
- {videoSubtitles && }
-
-
-
-
-
-
- >
- );
-};
-
-export default VideoPlayer;
+import updateVideoProgressById from '../api/actions/updateVideoProgressById';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
+import watchedThreshold from '../functions/watchedThreshold';
+import Notifications from './Notifications';
+import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useState } from 'react';
+import formatTime from '../functions/formatTime';
+import { useSearchParams } from 'react-router-dom';
+import getApiUrl from '../configuration/getApiUrl';
+
+type VideoTag = SyntheticEvent;
+
+export type SkippedSegmentType = {
+ from: number;
+ to: number;
+};
+
+export type SponsorSegmentsSkippedType = Record;
+
+type Subtitle = {
+ name: string;
+ source: string;
+ lang: string;
+ media_url: string;
+};
+
+type SubtitlesProp = {
+ subtitles: Subtitle[];
+};
+
+const Subtitles = ({ subtitles }: SubtitlesProp) => {
+ return subtitles.map((subtitle: Subtitle) => {
+ let label = subtitle.name;
+
+ if (subtitle.source === 'auto') {
+ label += ' - auto';
+ }
+
+ return (
+
+ );
+ });
+};
+
+const handleTimeUpdate =
+ (
+ youtubeId: string,
+ duration: number,
+ watched: boolean,
+ sponsorBlock?: SponsorBlockType,
+ setSponsorSegmentSkipped?: Dispatch>,
+ ) =>
+ async (videoTag: VideoTag) => {
+ const currentTime = Number(videoTag.currentTarget.currentTime);
+
+ if (sponsorBlock && sponsorBlock.segments) {
+ sponsorBlock.segments.forEach((segment: SponsorBlockSegmentType) => {
+ const [from, to] = segment.segment;
+
+ if (currentTime >= from && currentTime <= from + 0.3) {
+ videoTag.currentTarget.currentTime = to;
+
+ setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
+ return { ...segments, [segment.UUID]: { from, to } };
+ });
+ }
+
+ if (currentTime > to + 10) {
+ setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
+ return { ...segments, [segment.UUID]: { from: 0, to: 0 } };
+ });
+ }
+ });
+ }
+
+ if (currentTime < 10) return;
+ if (Number((currentTime % 10).toFixed(1)) <= 0.2) {
+ // Check progress every 10 seconds or else progress is checked a few times a second
+ await updateVideoProgressById({
+ youtubeId,
+ currentProgress: currentTime,
+ });
+
+ if (!watched) {
+ // Check if video is already marked as watched
+ if (watchedThreshold(currentTime, duration)) {
+ await updateWatchedState({
+ id: youtubeId,
+ is_watched: true,
+ });
+ }
+ }
+ }
+ };
+
+type VideoPlayerProps = {
+ video: VideoResponseType;
+ sponsorBlock?: SponsorBlockType;
+ embed?: boolean;
+ autoplay?: boolean;
+ onVideoEnd?: () => void;
+};
+
+const VideoPlayer = ({
+ video,
+ sponsorBlock,
+ embed,
+ autoplay = false,
+ onVideoEnd,
+}: VideoPlayerProps) => {
+ const [searchParams] = useSearchParams();
+ const searchParamVideoProgress = searchParams.get('t');
+
+ const [skippedSegments, setSkippedSegments] = useState({});
+
+ const videoId = video.data.youtube_id;
+ const videoUrl = video.data.media_url;
+ const videoThumbUrl = video.data.vid_thumb_url;
+ const watched = video.data.player.watched;
+ const duration = video.data.player.duration;
+ const videoSubtitles = video.data.subtitles;
+
+ let videoSrcProgress =
+ Number(video.data.player?.position) > 0 ? Number(video.data.player?.position) : '';
+
+ if (searchParamVideoProgress !== null) {
+ videoSrcProgress = searchParamVideoProgress;
+ }
+
+ const handleVideoEnd =
+ (
+ youtubeId: string,
+ watched: boolean,
+ setSponsorSegmentSkipped?: Dispatch>,
+ ) =>
+ async () => {
+ if (!watched) {
+ // Check if video is already marked as watched
+ await updateWatchedState({ id: youtubeId, is_watched: true });
+ }
+
+ setSponsorSegmentSkipped?.((segments: SponsorSegmentsSkippedType) => {
+ const keys = Object.keys(segments);
+
+ keys.forEach(uuid => {
+ segments[uuid] = { from: 0, to: 0 };
+ });
+
+ return segments;
+ });
+
+ onVideoEnd?.();
+ };
+
+ return (
+ <>
+
+
+ {
+ localStorage.setItem('playerVolume', videoTag.currentTarget.volume.toString());
+ }}
+ onRateChange={(videoTag: VideoTag) => {
+ localStorage.setItem('playerSpeed', videoTag.currentTarget.playbackRate.toString());
+ }}
+ onLoadStart={(videoTag: VideoTag) => {
+ videoTag.currentTarget.volume = Number(localStorage.getItem('playerVolume') ?? 1);
+ videoTag.currentTarget.playbackRate = Number(
+ localStorage.getItem('playerSpeed') ?? 1,
+ );
+ }}
+ onTimeUpdate={handleTimeUpdate(
+ videoId,
+ duration,
+ watched,
+ sponsorBlock,
+ setSkippedSegments,
+ )}
+ onPause={async (videoTag: VideoTag) => {
+ const currentTime = Number(videoTag.currentTarget.currentTime);
+
+ if (currentTime < 10) return;
+
+ await updateVideoProgressById({
+ youtubeId: videoId,
+ currentProgress: currentTime,
+ });
+ }}
+ onEnded={handleVideoEnd(videoId, watched)}
+ autoPlay={autoplay}
+ controls
+ width="100%"
+ playsInline
+ id="video-item"
+ >
+
+ {videoSubtitles && }
+
+
+
+
+
+
+ >
+ );
+};
+
+export default VideoPlayer;
diff --git a/frontend/src/configuration/colours/getColours.ts b/frontend/src/configuration/colours/getColours.ts
index 82cbec2d..af43781f 100644
--- a/frontend/src/configuration/colours/getColours.ts
+++ b/frontend/src/configuration/colours/getColours.ts
@@ -8,9 +8,8 @@ export const ColourConstant = {
};
const importColours = () => {
-
const { userConfig } = useUserConfigStore();
- const stylesheet = userConfig?.config.stylesheet
+ const stylesheet = userConfig?.config.stylesheet;
switch (stylesheet) {
case ColourConstant.Dark:
diff --git a/frontend/src/functions/getIsAdmin.ts b/frontend/src/functions/getIsAdmin.ts
index 6efe0441..ca4c2ce8 100644
--- a/frontend/src/functions/getIsAdmin.ts
+++ b/frontend/src/functions/getIsAdmin.ts
@@ -1,7 +1,7 @@
import { useUserConfigStore } from '../stores/UserConfigStore';
const loadIsAdmin = () => {
- const { userConfig } = useUserConfigStore()
+ const { userConfig } = useUserConfigStore();
const isAdmin = userConfig?.is_staff || userConfig?.is_superuser;
return isAdmin;
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index d3a1b6f1..f3f37e89 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,144 +1,144 @@
-import * as React from 'react';
-import * as ReactDOM from 'react-dom/client';
-import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom';
-import Routes from './configuration/routes/RouteList';
-import './style.css';
-import Base from './pages/Base';
-import About from './pages/About';
-import Channels from './pages/Channels';
-import ErrorPage from './pages/ErrorPage';
-import Home from './pages/Home';
-import Playlist from './pages/Playlist';
-import Playlists from './pages/Playlists';
-import Search from './pages/Search';
-import SettingsDashboard from './pages/SettingsDashboard';
-import Video from './pages/Video';
-import Login from './pages/Login';
-import SettingsActions from './pages/SettingsActions';
-import SettingsApplication from './pages/SettingsApplication';
-import SettingsScheduling from './pages/SettingsScheduling';
-import SettingsUser from './pages/SettingsUser';
-import loadUserMeConfig from './api/loader/loadUserConfig';
-import loadAuth from './api/loader/loadAuth';
-import ChannelBase from './pages/ChannelBase';
-import ChannelVideo from './pages/ChannelVideo';
-import ChannelPlaylist from './pages/ChannelPlaylist';
-import ChannelAbout from './pages/ChannelAbout';
-import Download from './pages/Download';
-
-const router = createBrowserRouter(
- [
- {
- path: Routes.Home,
- loader: async () => {
- console.log('------------ after reload');
-
- const auth = await loadAuth();
- if (auth.status === 403) {
- return redirect(Routes.Login);
- }
-
- const authData = await auth.json();
-
- const userConfig = await loadUserMeConfig();
-
- return { userConfig, auth: authData };
- },
- element: ,
- errorElement: ,
- children: [
- {
- index: true,
- element: ,
- },
- {
- path: Routes.Video(':videoId'),
- element: ,
- },
- {
- path: Routes.Channels,
- element: ,
- },
- {
- path: Routes.Channel(':channelId'),
- element: ,
- children: [
- {
- index: true,
- path: Routes.ChannelVideo(':channelId'),
- element: ,
- },
- {
- path: Routes.ChannelStream(':channelId'),
- element: ,
- },
- {
- path: Routes.ChannelShorts(':channelId'),
- element: ,
- },
- {
- path: Routes.ChannelPlaylist(':channelId'),
- element: ,
- },
- {
- path: Routes.ChannelAbout(':channelId'),
- element: ,
- },
- ],
- },
- {
- path: Routes.Playlists,
- element: ,
- },
- {
- path: Routes.Playlist(':playlistId'),
- element: ,
- },
- {
- path: Routes.Downloads,
- element: ,
- },
- {
- path: Routes.Search,
- element: ,
- },
- {
- path: Routes.SettingsDashboard,
- element: ,
- },
- {
- path: Routes.SettingsActions,
- element: ,
- },
- {
- path: Routes.SettingsApplication,
- element: ,
- },
- {
- path: Routes.SettingsScheduling,
- element: ,
- },
- {
- path: Routes.SettingsUser,
- element: ,
- },
- {
- path: Routes.About,
- element: ,
- },
- ],
- },
- {
- path: Routes.Login,
- element: ,
- errorElement: ,
- },
- ],
- { basename: import.meta.env.BASE_URL },
-);
-
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
- ,
-);
+import * as React from 'react';
+import * as ReactDOM from 'react-dom/client';
+import { createBrowserRouter, redirect, RouterProvider } from 'react-router-dom';
+import Routes from './configuration/routes/RouteList';
+import './style.css';
+import Base from './pages/Base';
+import About from './pages/About';
+import Channels from './pages/Channels';
+import ErrorPage from './pages/ErrorPage';
+import Home from './pages/Home';
+import Playlist from './pages/Playlist';
+import Playlists from './pages/Playlists';
+import Search from './pages/Search';
+import SettingsDashboard from './pages/SettingsDashboard';
+import Video from './pages/Video';
+import Login from './pages/Login';
+import SettingsActions from './pages/SettingsActions';
+import SettingsApplication from './pages/SettingsApplication';
+import SettingsScheduling from './pages/SettingsScheduling';
+import SettingsUser from './pages/SettingsUser';
+import loadUserMeConfig from './api/loader/loadUserConfig';
+import loadAuth from './api/loader/loadAuth';
+import ChannelBase from './pages/ChannelBase';
+import ChannelVideo from './pages/ChannelVideo';
+import ChannelPlaylist from './pages/ChannelPlaylist';
+import ChannelAbout from './pages/ChannelAbout';
+import Download from './pages/Download';
+
+const router = createBrowserRouter(
+ [
+ {
+ path: Routes.Home,
+ loader: async () => {
+ console.log('------------ after reload');
+
+ const auth = await loadAuth();
+ if (auth.status === 403) {
+ return redirect(Routes.Login);
+ }
+
+ const authData = await auth.json();
+
+ const userConfig = await loadUserMeConfig();
+
+ return { userConfig, auth: authData };
+ },
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: Routes.Video(':videoId'),
+ element: ,
+ },
+ {
+ path: Routes.Channels,
+ element: ,
+ },
+ {
+ path: Routes.Channel(':channelId'),
+ element: ,
+ children: [
+ {
+ index: true,
+ path: Routes.ChannelVideo(':channelId'),
+ element: ,
+ },
+ {
+ path: Routes.ChannelStream(':channelId'),
+ element: ,
+ },
+ {
+ path: Routes.ChannelShorts(':channelId'),
+ element: ,
+ },
+ {
+ path: Routes.ChannelPlaylist(':channelId'),
+ element: ,
+ },
+ {
+ path: Routes.ChannelAbout(':channelId'),
+ element: ,
+ },
+ ],
+ },
+ {
+ path: Routes.Playlists,
+ element: ,
+ },
+ {
+ path: Routes.Playlist(':playlistId'),
+ element: ,
+ },
+ {
+ path: Routes.Downloads,
+ element: ,
+ },
+ {
+ path: Routes.Search,
+ element: ,
+ },
+ {
+ path: Routes.SettingsDashboard,
+ element: ,
+ },
+ {
+ path: Routes.SettingsActions,
+ element: ,
+ },
+ {
+ path: Routes.SettingsApplication,
+ element: ,
+ },
+ {
+ path: Routes.SettingsScheduling,
+ element: ,
+ },
+ {
+ path: Routes.SettingsUser,
+ element: ,
+ },
+ {
+ path: Routes.About,
+ element: ,
+ },
+ ],
+ },
+ {
+ path: Routes.Login,
+ element: ,
+ errorElement: ,
+ },
+ ],
+ { basename: import.meta.env.BASE_URL },
+);
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index aac50539..928f8915 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -1,60 +1,60 @@
-const About = () => {
- return (
- <>
- TA | About
-
-
-
-
About The Tube Archivist
-
-
-
Useful Links
-
- This project is in active and constant development, take a look at the{' '}
-
- roadmap
- {' '}
- for a overview.
-
-
- All functionality is documented in our up-to-date{' '}
-
- user guide
-
- .
-
-
- All contributions are welcome: Open an{' '}
-
- issue
- {' '}
- for any bugs and errors, join us on{' '}
-
- Discord
- {' '}
- to discuss details. The{' '}
-
- contributing
- {' '}
- page is a good place to get started.
-
-
-
-
Donate
-
- Here are{' '}
-
- some links
-
- , if you want to buy the developer a coffee. Thank you for your support!
-
-
-
- >
- );
-};
-
-export default About;
+const About = () => {
+ return (
+ <>
+ TA | About
+
+
+
+
About The Tube Archivist
+
+
+
Useful Links
+
+ This project is in active and constant development, take a look at the{' '}
+
+ roadmap
+ {' '}
+ for a overview.
+
+
+ All functionality is documented in our up-to-date{' '}
+
+ user guide
+
+ .
+
+
+ All contributions are welcome: Open an{' '}
+
+ issue
+ {' '}
+ for any bugs and errors, join us on{' '}
+
+ Discord
+ {' '}
+ to discuss details. The{' '}
+
+ contributing
+ {' '}
+ page is a good place to get started.
+
+
+
+
Donate
+
+ Here are{' '}
+
+ some links
+
+ , if you want to buy the developer a coffee. Thank you for your support!
+
+
+
+ >
+ );
+};
+
+export default About;
diff --git a/frontend/src/pages/Base.tsx b/frontend/src/pages/Base.tsx
index 5e4991d8..00724b36 100644
--- a/frontend/src/pages/Base.tsx
+++ b/frontend/src/pages/Base.tsx
@@ -31,7 +31,7 @@ export type OutletContextType = {
const Base = () => {
const { setAuth } = useAuthStore();
- const { setUserConfig } = useUserConfigStore()
+ const { setUserConfig } = useUserConfigStore();
const { userConfig, auth } = useLoaderData() as BaseLoaderData;
const location = useLocation();
@@ -46,7 +46,7 @@ const Base = () => {
useEffect(() => {
setAuth(auth);
setUserConfig(userConfig);
- }, [])
+ }, []);
useEffect(() => {
if (currentPageFromUrl !== currentPage) {
diff --git a/frontend/src/pages/ChannelBase.tsx b/frontend/src/pages/ChannelBase.tsx
index a29b0723..e696a159 100644
--- a/frontend/src/pages/ChannelBase.tsx
+++ b/frontend/src/pages/ChannelBase.tsx
@@ -1,105 +1,105 @@
-import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import { ChannelType } from './Channels';
-import { ConfigType } from './Home';
-import { OutletContextType } from './Base';
-import Notifications from '../components/Notifications';
-import { useEffect, useState } from 'react';
-import ChannelBanner from '../components/ChannelBanner';
-import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
-import loadChannelById from '../api/loader/loadChannelById';
-import loadIsAdmin from '../functions/getIsAdmin';
-
-type ChannelParams = {
- channelId: string;
-};
-
-export type ChannelResponseType = {
- data: ChannelType;
- config: ConfigType;
-};
-
-const ChannelBase = () => {
- const { channelId } = useParams() as ChannelParams;
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const isAdmin = loadIsAdmin();
-
- const [channelResponse, setChannelResponse] = useState();
- const [channelNav, setChannelNav] = useState();
- const [startNotification, setStartNotification] = useState(false);
-
- const channel = channelResponse?.data;
- const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
-
- useEffect(() => {
- (async () => {
- const channelNavResponse = await loadChannelNav(channelId);
- const channelResponse = await loadChannelById(channelId);
-
- setChannelResponse(channelResponse);
- setChannelNav(channelNavResponse);
- })();
- }, [channelId]);
-
- if (!channelId) {
- return [];
- }
-
- return (
- <>
-
-
-
-
-
-
-
-
-
Videos
-
- {has_streams && (
-
- Streams
-
- )}
- {has_shorts && (
-
- Shorts
-
- )}
- {has_playlists && (
-
- Playlists
-
- )}
-
- About
-
- {has_pending && isAdmin && (
-
- Downloads
-
- )}
-
-
-
setStartNotification(false)}
- />
-
-
-
- >
- );
-};
-
-export default ChannelBase;
+import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import { ChannelType } from './Channels';
+import { ConfigType } from './Home';
+import { OutletContextType } from './Base';
+import Notifications from '../components/Notifications';
+import { useEffect, useState } from 'react';
+import ChannelBanner from '../components/ChannelBanner';
+import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav';
+import loadChannelById from '../api/loader/loadChannelById';
+import loadIsAdmin from '../functions/getIsAdmin';
+
+type ChannelParams = {
+ channelId: string;
+};
+
+export type ChannelResponseType = {
+ data: ChannelType;
+ config: ConfigType;
+};
+
+const ChannelBase = () => {
+ const { channelId } = useParams() as ChannelParams;
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const isAdmin = loadIsAdmin();
+
+ const [channelResponse, setChannelResponse] = useState();
+ const [channelNav, setChannelNav] = useState();
+ const [startNotification, setStartNotification] = useState(false);
+
+ const channel = channelResponse?.data;
+ const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
+
+ useEffect(() => {
+ (async () => {
+ const channelNavResponse = await loadChannelNav(channelId);
+ const channelResponse = await loadChannelById(channelId);
+
+ setChannelResponse(channelResponse);
+ setChannelNav(channelNavResponse);
+ })();
+ }, [channelId]);
+
+ if (!channelId) {
+ return [];
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
Videos
+
+ {has_streams && (
+
+ Streams
+
+ )}
+ {has_shorts && (
+
+ Shorts
+
+ )}
+ {has_playlists && (
+
+ Playlists
+
+ )}
+
+ About
+
+ {has_pending && isAdmin && (
+
+ Downloads
+
+ )}
+
+
+
setStartNotification(false)}
+ />
+
+
+
+ >
+ );
+};
+
+export default ChannelBase;
diff --git a/frontend/src/pages/ChannelPlaylist.tsx b/frontend/src/pages/ChannelPlaylist.tsx
index 0206d817..3f4846a1 100644
--- a/frontend/src/pages/ChannelPlaylist.tsx
+++ b/frontend/src/pages/ChannelPlaylist.tsx
@@ -1,107 +1,104 @@
-import { useOutletContext, useParams } from 'react-router-dom';
-import Notifications from '../components/Notifications';
-import PlaylistList from '../components/PlaylistList';
-import { useEffect, useState } from 'react';
-import { OutletContextType } from './Base';
-import Pagination from '../components/Pagination';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import loadPlaylistList from '../api/loader/loadPlaylistList';
-import { PlaylistsResponseType } from './Playlists';
-import iconGridView from '/img/icon-gridview.svg';
-import iconListView from '/img/icon-listview.svg';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-const ChannelPlaylist = () => {
- const { channelId } = useParams();
- const { userConfig, setPartialConfig } = useUserConfigStore();
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
-
- const [refreshPlaylists, setRefreshPlaylists] = useState(false);
-
- const [playlistsResponse, setPlaylistsResponse] = useState();
-
- const playlistList = playlistsResponse?.data;
- const pagination = playlistsResponse?.paginate;
-
- const view = userConfig.config.view_style_playlist;
- const showSubedOnly = userConfig.config.show_subed_only;
-
- useEffect(() => {
- (async () => {
- const playlists = await loadPlaylistList({
- channel: channelId,
- subscribed: showSubedOnly,
- });
-
- setPlaylistsResponse(playlists);
- setRefreshPlaylists(false);
- })();
- }, [channelId, refreshPlaylists, showSubedOnly, currentPage]);
-
- return (
- <>
- TA | Channel: Playlists
-
-
-
-
-
-
-
Show subscribed only:
-
- {
- setPartialConfig({show_subed_only: !showSubedOnly});
- setRefreshPlaylists(true);
- }}
- type="checkbox"
- />
- {!showSubedOnly && (
-
- Off
-
- )}
- {showSubedOnly && (
-
- On
-
- )}
-
-
-
-
{
- setPartialConfig({view_style_playlist: 'grid'});
- }}
- alt="grid view"
- />
-
{
- setPartialConfig({view_style_playlist: 'list'});
- }}
- alt="list view"
- />
-
-
-
-
-
-
-
- >
- );
-};
-
-export default ChannelPlaylist;
+import { useOutletContext, useParams } from 'react-router-dom';
+import Notifications from '../components/Notifications';
+import PlaylistList from '../components/PlaylistList';
+import { useEffect, useState } from 'react';
+import { OutletContextType } from './Base';
+import Pagination from '../components/Pagination';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import loadPlaylistList from '../api/loader/loadPlaylistList';
+import { PlaylistsResponseType } from './Playlists';
+import iconGridView from '/img/icon-gridview.svg';
+import iconListView from '/img/icon-listview.svg';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+const ChannelPlaylist = () => {
+ const { channelId } = useParams();
+ const { userConfig, setPartialConfig } = useUserConfigStore();
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+
+ const [refreshPlaylists, setRefreshPlaylists] = useState(false);
+
+ const [playlistsResponse, setPlaylistsResponse] = useState();
+
+ const playlistList = playlistsResponse?.data;
+ const pagination = playlistsResponse?.paginate;
+
+ const view = userConfig.config.view_style_playlist;
+ const showSubedOnly = userConfig.config.show_subed_only;
+
+ useEffect(() => {
+ (async () => {
+ const playlists = await loadPlaylistList({
+ channel: channelId,
+ subscribed: showSubedOnly,
+ });
+
+ setPlaylistsResponse(playlists);
+ setRefreshPlaylists(false);
+ })();
+ }, [channelId, refreshPlaylists, showSubedOnly, currentPage]);
+
+ return (
+ <>
+ TA | Channel: Playlists
+
+
+
+
+
+
+
Show subscribed only:
+
+ {
+ setPartialConfig({ show_subed_only: !showSubedOnly });
+ setRefreshPlaylists(true);
+ }}
+ type="checkbox"
+ />
+ {!showSubedOnly && (
+
+ Off
+
+ )}
+ {showSubedOnly && (
+
+ On
+
+ )}
+
+
+
+
{
+ setPartialConfig({ view_style_playlist: 'grid' });
+ }}
+ alt="grid view"
+ />
+
{
+ setPartialConfig({ view_style_playlist: 'list' });
+ }}
+ alt="list view"
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ChannelPlaylist;
diff --git a/frontend/src/pages/ChannelVideo.tsx b/frontend/src/pages/ChannelVideo.tsx
index 7903a3e1..a4709b4a 100644
--- a/frontend/src/pages/ChannelVideo.tsx
+++ b/frontend/src/pages/ChannelVideo.tsx
@@ -1,193 +1,188 @@
-import { useEffect, useState } from 'react';
-import {
- Link,
- useOutletContext,
- useParams,
- useSearchParams,
-} from 'react-router-dom';
-import { OutletContextType } from './Base';
-import VideoList from '../components/VideoList';
-import Routes from '../configuration/routes/RouteList';
-import Pagination from '../components/Pagination';
-import Filterbar from '../components/Filterbar';
-import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
-import ChannelOverview from '../components/ChannelOverview';
-import loadChannelById from '../api/loader/loadChannelById';
-import { ChannelResponseType } from './ChannelBase';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import Button from '../components/Button';
-import loadVideoListByFilter, {
- VideoListByFilterResponseType,
- VideoTypes,
-} from '../api/loader/loadVideoListByPage';
-import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
-import humanFileSize from '../functions/humanFileSize';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-type ChannelParams = {
- channelId: string;
-};
-
-type ChannelVideoProps = {
- videoType: VideoTypes;
-};
-
-const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
- const { channelId } = useParams() as ChannelParams;
- const { userConfig } = useUserConfigStore();
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
-
- const [refresh, setRefresh] = useState(false);
-
- const [channelResponse, setChannelResponse] = useState();
- const [videoResponse, setVideoReponse] = useState();
- const [videoAggsResponse, setVideoAggsResponse] = useState();
-
- const channel = channelResponse?.data;
- const videoList = videoResponse?.data;
- const pagination = videoResponse?.paginate;
-
- const hasVideos = videoResponse?.data?.length !== 0;
- const showEmbeddedVideo = videoId !== null;
-
- const view = userConfig.config.view_style_home
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${userConfig.config.grid_items}` : '';
- const gridViewGrid = isGridView ? `grid-${userConfig.config.grid_items}` : '';
-
- useEffect(() => {
- (async () => {
- const channelResponse = await loadChannelById(channelId);
- const videos = await loadVideoListByFilter({
- channel: channelId,
- page: currentPage,
- watch: userConfig.config.hide_watched ? 'unwatched' : undefined,
- sort: userConfig.config.sort_by,
- order: userConfig.config.sort_order,
- type: videoType,
- });
- const channelAggs = await loadChannelAggs(channelId);
-
- setChannelResponse(channelResponse);
- setVideoReponse(videos);
- setVideoAggsResponse(channelAggs);
- setRefresh(false);
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- refresh,
- userConfig.config.sort_by,
- userConfig.config.sort_order,
- userConfig.config.hide_watched,
- currentPage,
- channelId,
- pagination?.current_page,
- videoType,
- ]);
-
- if (!channel) {
- return (
-
-
-
Channel {channelId} not found!
-
- );
- }
-
- return (
- <>
- {`TA | Channel: ${channel.channel_name}`}
-
-
-
-
-
- {videoAggsResponse && (
- <>
-
- {videoAggsResponse.total_items.value} videos{' '}
- | {' '}
- {videoAggsResponse.total_duration.value_str} playback{' '}
- | Total size{' '}
- {humanFileSize(videoAggsResponse.total_size.value, true)}
-
-
- {
- await updateWatchedState({
- id: channel.channel_id,
- is_watched: true,
- });
-
- setRefresh(true);
- }}
- />{' '}
- {
- await updateWatchedState({
- id: channel.channel_id,
- is_watched: false,
- });
-
- setRefresh(true);
- }}
- />
-
- >
- )}
-
-
-
-
-
-
- {showEmbeddedVideo && }
-
-
- {!hasVideos && (
- <>
-
No videos found...
-
- Try going to the downloads page to start the scan
- and download tasks.
-
- >
- )}
-
-
-
-
- {pagination && (
-
- )}
- >
- );
-};
-
-export default ChannelVideo;
+import { useEffect, useState } from 'react';
+import { Link, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
+import { OutletContextType } from './Base';
+import VideoList from '../components/VideoList';
+import Routes from '../configuration/routes/RouteList';
+import Pagination from '../components/Pagination';
+import Filterbar from '../components/Filterbar';
+import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
+import ChannelOverview from '../components/ChannelOverview';
+import loadChannelById from '../api/loader/loadChannelById';
+import { ChannelResponseType } from './ChannelBase';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import Button from '../components/Button';
+import loadVideoListByFilter, {
+ VideoListByFilterResponseType,
+ VideoTypes,
+} from '../api/loader/loadVideoListByPage';
+import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs';
+import humanFileSize from '../functions/humanFileSize';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+type ChannelParams = {
+ channelId: string;
+};
+
+type ChannelVideoProps = {
+ videoType: VideoTypes;
+};
+
+const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
+ const { channelId } = useParams() as ChannelParams;
+ const { userConfig } = useUserConfigStore();
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+
+ const [refresh, setRefresh] = useState(false);
+
+ const [channelResponse, setChannelResponse] = useState();
+ const [videoResponse, setVideoReponse] = useState();
+ const [videoAggsResponse, setVideoAggsResponse] = useState();
+
+ const channel = channelResponse?.data;
+ const videoList = videoResponse?.data;
+ const pagination = videoResponse?.paginate;
+
+ const hasVideos = videoResponse?.data?.length !== 0;
+ const showEmbeddedVideo = videoId !== null;
+
+ const view = userConfig.config.view_style_home;
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${userConfig.config.grid_items}` : '';
+ const gridViewGrid = isGridView ? `grid-${userConfig.config.grid_items}` : '';
+
+ useEffect(() => {
+ (async () => {
+ const channelResponse = await loadChannelById(channelId);
+ const videos = await loadVideoListByFilter({
+ channel: channelId,
+ page: currentPage,
+ watch: userConfig.config.hide_watched ? 'unwatched' : undefined,
+ sort: userConfig.config.sort_by,
+ order: userConfig.config.sort_order,
+ type: videoType,
+ });
+ const channelAggs = await loadChannelAggs(channelId);
+
+ setChannelResponse(channelResponse);
+ setVideoReponse(videos);
+ setVideoAggsResponse(channelAggs);
+ setRefresh(false);
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ refresh,
+ userConfig.config.sort_by,
+ userConfig.config.sort_order,
+ userConfig.config.hide_watched,
+ currentPage,
+ channelId,
+ pagination?.current_page,
+ videoType,
+ ]);
+
+ if (!channel) {
+ return (
+
+
+
Channel {channelId} not found!
+
+ );
+ }
+
+ return (
+ <>
+ {`TA | Channel: ${channel.channel_name}`}
+
+
+
+
+
+ {videoAggsResponse && (
+ <>
+
+ {videoAggsResponse.total_items.value} videos{' '}
+ | {' '}
+ {videoAggsResponse.total_duration.value_str} playback{' '}
+ | Total size{' '}
+ {humanFileSize(videoAggsResponse.total_size.value, true)}
+
+
+ {
+ await updateWatchedState({
+ id: channel.channel_id,
+ is_watched: true,
+ });
+
+ setRefresh(true);
+ }}
+ />{' '}
+ {
+ await updateWatchedState({
+ id: channel.channel_id,
+ is_watched: false,
+ });
+
+ setRefresh(true);
+ }}
+ />
+
+ >
+ )}
+
+
+
+
+
+
+ {showEmbeddedVideo && }
+
+
+ {!hasVideos && (
+ <>
+
No videos found...
+
+ Try going to the downloads page to start the scan
+ and download tasks.
+
+ >
+ )}
+
+
+
+
+ {pagination && (
+
+ )}
+ >
+ );
+};
+
+export default ChannelVideo;
diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx
index f5c26fba..86a9fd91 100644
--- a/frontend/src/pages/Channels.tsx
+++ b/frontend/src/pages/Channels.tsx
@@ -1,189 +1,190 @@
-import { useOutletContext } from 'react-router-dom';
-import loadChannelList from '../api/loader/loadChannelList';
-import iconGridView from '/img/icon-gridview.svg';
-import iconListView from '/img/icon-listview.svg';
-import iconAdd from '/img/icon-add.svg';
-import { useEffect, useState } from 'react';
-import Pagination, { PaginationType } from '../components/Pagination';
-import { ConfigType } from './Home';
-import { OutletContextType } from './Base';
-import ChannelList from '../components/ChannelList';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import Notifications from '../components/Notifications';
-import Button from '../components/Button';
-import updateChannelSubscription from '../api/actions/updateChannelSubscription';
-import loadIsAdmin from '../functions/getIsAdmin';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-type ChannelOverwritesType = {
- download_format?: string;
- autodelete_days?: number;
- index_playlists?: boolean;
- integrate_sponsorblock?: boolean;
- subscriptions_channel_size?: number;
- subscriptions_live_channel_size?: number;
- subscriptions_shorts_channel_size?: number;
-};
-
-export type ChannelType = {
- channel_active: boolean;
- channel_banner_url: string;
- channel_description: string;
- channel_id: string;
- channel_last_refresh: string;
- channel_name: string;
- channel_overwrites?: ChannelOverwritesType;
- channel_subs: number;
- channel_subscribed: boolean;
- channel_tags: string[];
- channel_thumb_url: string;
- channel_tvart_url: string;
- channel_views: number;
-};
-
-type ChannelsListResponse = {
- data: ChannelType[];
- paginate: PaginationType;
- config?: ConfigType;
-};
-
-const Channels = () => {
- const { userConfig, setPartialConfig } = useUserConfigStore();
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const isAdmin = loadIsAdmin();
-
- const [channelListResponse, setChannelListResponse] = useState();
- const [showAddForm, setShowAddForm] = useState(false);
- const [refresh, setRefresh] = useState(false);
- const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
-
- const channels = channelListResponse?.data;
- const pagination = channelListResponse?.paginate;
- const channelCount = pagination?.total_hits;
- const hasChannels = channels?.length !== 0;
-
- useEffect(() => {
- (async () => {
- const channelListResponse = await loadChannelList(currentPage, userConfig.config.show_subed_only);
- setChannelListResponse(channelListResponse);
- })();
- }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
-
- return (
- <>
- TA | Channels
-
-
-
-
-
Channels
-
- {isAdmin && (
-
-
{
- setShowAddForm(!showAddForm);
- }}
- src={iconAdd}
- alt="add-icon"
- title="Subscribe to Channels"
- />
- {showAddForm && (
-
-
- Subscribe to channels:
-
-
-
{
- await updateChannelSubscription(channelsToSubscribeTo, true);
-
- setRefresh(true);
- }}
- />
-
- )}
-
- )}
-
-
-
-
-
-
-
Show subscribed only:
-
- {
- setPartialConfig({show_subed_only: !userConfig.config.show_subed_only});
- setRefresh(true);
- }}
- type="checkbox"
- checked={userConfig.config.show_subed_only}
- />
- {!userConfig.config.show_subed_only && (
-
- Off
-
- )}
- {userConfig.config.show_subed_only && (
-
- On
-
- )}
-
-
-
-
{
- setPartialConfig({view_style_channel: 'grid'});
- }}
- data-origin="channel"
- data-value="grid"
- alt="grid view"
- />
-
{
- setPartialConfig({view_style_channel: 'list'});
- }}
- data-origin="channel"
- data-value="list"
- alt="list view"
- />
-
-
- {hasChannels &&
Total channels: {channelCount} }
-
-
- {!hasChannels &&
No channels found... }
-
- {hasChannels && (
-
- )}
-
-
- {pagination && (
-
- )}
-
- >
- );
-};
-
-export default Channels;
+import { useOutletContext } from 'react-router-dom';
+import loadChannelList from '../api/loader/loadChannelList';
+import iconGridView from '/img/icon-gridview.svg';
+import iconListView from '/img/icon-listview.svg';
+import iconAdd from '/img/icon-add.svg';
+import { useEffect, useState } from 'react';
+import Pagination, { PaginationType } from '../components/Pagination';
+import { ConfigType } from './Home';
+import { OutletContextType } from './Base';
+import ChannelList from '../components/ChannelList';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import Notifications from '../components/Notifications';
+import Button from '../components/Button';
+import updateChannelSubscription from '../api/actions/updateChannelSubscription';
+import loadIsAdmin from '../functions/getIsAdmin';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+type ChannelOverwritesType = {
+ download_format?: string;
+ autodelete_days?: number;
+ index_playlists?: boolean;
+ integrate_sponsorblock?: boolean;
+ subscriptions_channel_size?: number;
+ subscriptions_live_channel_size?: number;
+ subscriptions_shorts_channel_size?: number;
+};
+
+export type ChannelType = {
+ channel_active: boolean;
+ channel_banner_url: string;
+ channel_description: string;
+ channel_id: string;
+ channel_last_refresh: string;
+ channel_name: string;
+ channel_overwrites?: ChannelOverwritesType;
+ channel_subs: number;
+ channel_subscribed: boolean;
+ channel_tags: string[];
+ channel_thumb_url: string;
+ channel_tvart_url: string;
+ channel_views: number;
+};
+
+type ChannelsListResponse = {
+ data: ChannelType[];
+ paginate: PaginationType;
+ config?: ConfigType;
+};
+
+const Channels = () => {
+ const { userConfig, setPartialConfig } = useUserConfigStore();
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const isAdmin = loadIsAdmin();
+
+ const [channelListResponse, setChannelListResponse] = useState();
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [refresh, setRefresh] = useState(false);
+ const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
+
+ const channels = channelListResponse?.data;
+ const pagination = channelListResponse?.paginate;
+ const channelCount = pagination?.total_hits;
+ const hasChannels = channels?.length !== 0;
+
+ useEffect(() => {
+ (async () => {
+ const channelListResponse = await loadChannelList(
+ currentPage,
+ userConfig.config.show_subed_only,
+ );
+ setChannelListResponse(channelListResponse);
+ })();
+ }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
+
+ return (
+ <>
+ TA | Channels
+
+
+
+
+
Channels
+
+ {isAdmin && (
+
+
{
+ setShowAddForm(!showAddForm);
+ }}
+ src={iconAdd}
+ alt="add-icon"
+ title="Subscribe to Channels"
+ />
+ {showAddForm && (
+
+
+ Subscribe to channels:
+
+
+
{
+ await updateChannelSubscription(channelsToSubscribeTo, true);
+
+ setRefresh(true);
+ }}
+ />
+
+ )}
+
+ )}
+
+
+
+
+
+
+
Show subscribed only:
+
+ {
+ setPartialConfig({ show_subed_only: !userConfig.config.show_subed_only });
+ setRefresh(true);
+ }}
+ type="checkbox"
+ checked={userConfig.config.show_subed_only}
+ />
+ {!userConfig.config.show_subed_only && (
+
+ Off
+
+ )}
+ {userConfig.config.show_subed_only && (
+
+ On
+
+ )}
+
+
+
+
{
+ setPartialConfig({ view_style_channel: 'grid' });
+ }}
+ data-origin="channel"
+ data-value="grid"
+ alt="grid view"
+ />
+
{
+ setPartialConfig({ view_style_channel: 'list' });
+ }}
+ data-origin="channel"
+ data-value="list"
+ alt="list view"
+ />
+
+
+ {hasChannels &&
Total channels: {channelCount} }
+
+
+ {!hasChannels &&
No channels found... }
+
+ {hasChannels && }
+
+
+ {pagination && (
+
+ )}
+
+ >
+ );
+};
+
+export default Channels;
diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx
index fe70b1be..d3422cf7 100644
--- a/frontend/src/pages/Download.tsx
+++ b/frontend/src/pages/Download.tsx
@@ -206,7 +206,7 @@ const Download = () => {
{
- setPartialConfig({show_ignored_only: !showIgnored});
+ setPartialConfig({ show_ignored_only: !showIgnored });
setRefresh(true);
}}
type="checkbox"
@@ -262,7 +262,7 @@ const Download = () => {
{
- setPartialConfig({grid_items: gridItems + 1});
+ setPartialConfig({ grid_items: gridItems + 1 });
}}
alt="grid plus row"
/>
@@ -271,7 +271,7 @@ const Download = () => {
{
- setPartialConfig({grid_items: gridItems - 1});
+ setPartialConfig({ grid_items: gridItems - 1 });
}}
alt="grid minus row"
/>
@@ -282,14 +282,14 @@ const Download = () => {
{
- setPartialConfig({view_style_downloads: 'grid'});
+ setPartialConfig({ view_style_downloads: 'grid' });
}}
alt="grid view"
/>
{
- setPartialConfig({view_style_downloads: 'list'});
+ setPartialConfig({ view_style_downloads: 'list' });
}}
alt="list view"
/>
@@ -313,10 +313,7 @@ const Download = () => {
downloadList?.map(download => {
return (
-
+
);
})}
diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx
index 8022a657..e060b1cc 100644
--- a/frontend/src/pages/ErrorPage.tsx
+++ b/frontend/src/pages/ErrorPage.tsx
@@ -1,33 +1,32 @@
-import { useRouteError } from 'react-router-dom';
-import importColours from '../configuration/colours/getColours';
-
-
-// This is not always the correct response
-type ErrorType = {
- statusText: string;
- message: string;
-};
-
-const ErrorPage = () => {
- const error = useRouteError() as ErrorType;
- importColours();
-
- console.error('ErrorPage', error);
-
- return (
- <>
- TA | Oops!
-
-
-
Oops!
-
Sorry, an unexpected error has occurred.
-
- {error?.statusText}
- {error?.message}
-
-
- >
- );
-};
-
-export default ErrorPage;
+import { useRouteError } from 'react-router-dom';
+import importColours from '../configuration/colours/getColours';
+
+// This is not always the correct response
+type ErrorType = {
+ statusText: string;
+ message: string;
+};
+
+const ErrorPage = () => {
+ const error = useRouteError() as ErrorType;
+ importColours();
+
+ console.error('ErrorPage', error);
+
+ return (
+ <>
+ TA | Oops!
+
+
+
Oops!
+
Sorry, an unexpected error has occurred.
+
+ {error?.statusText}
+ {error?.message}
+
+
+ >
+ );
+};
+
+export default ErrorPage;
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 847c3375..315c41fe 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -1,232 +1,232 @@
-import { useEffect, useState } from 'react';
-import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
-import Routes from '../configuration/routes/RouteList';
-import Pagination from '../components/Pagination';
-import loadVideoListByFilter, {
- VideoListByFilterResponseType,
-} from '../api/loader/loadVideoListByPage';
-import VideoList from '../components/VideoList';
-import { ChannelType } from './Channels';
-import { OutletContextType } from './Base';
-import Filterbar from '../components/Filterbar';
-import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import { SponsorBlockType } from './Video';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-export type PlayerType = {
- watched: boolean;
- duration: number;
- duration_str: string;
- progress: number;
- position: number;
-};
-
-export type StatsType = {
- view_count: number;
- like_count: number;
- dislike_count: number;
- average_rating: number;
-};
-
-export type StreamType = {
- type: string;
- index: number;
- codec: string;
- width?: number;
- height?: number;
- bitrate: number;
-};
-
-export type Subtitles = {
- ext: string;
- url: string;
- name: string;
- lang: string;
- source: string;
- media_url: string;
-};
-
-export type VideoType = {
- active: boolean;
- category: string[];
- channel: ChannelType;
- date_downloaded: number;
- description: string;
- comment_count?: number;
- media_size: number;
- media_url: string;
- player: PlayerType;
- published: string;
- sponsorblock?: SponsorBlockType;
- playlist?: string[];
- stats: StatsType;
- streams: StreamType[];
- subtitles: Subtitles[];
- tags: string[];
- title: string;
- vid_last_refresh: string;
- vid_thumb_base64: boolean;
- vid_thumb_url: string;
- vid_type: string;
- youtube_id: string;
-};
-
-export type DownloadsType = {
- limit_speed: boolean;
- sleep_interval: number;
- autodelete_days: boolean;
- format: boolean;
- format_sort: boolean;
- add_metadata: boolean;
- add_thumbnail: boolean;
- subtitle: boolean;
- subtitle_source: boolean;
- subtitle_index: boolean;
- comment_max: boolean;
- comment_sort: string;
- cookie_import: boolean;
- throttledratelimit: boolean;
- extractor_lang: boolean;
- integrate_ryd: boolean;
- integrate_sponsorblock: boolean;
-};
-
-export type ConfigType = {
- enable_cast: boolean;
- downloads: DownloadsType;
-};
-
-export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
-export type SortOrderType = 'asc' | 'desc';
-export type ViewLayoutType = 'grid' | 'list';
-
-const Home = () => {
- const { userConfig } = useUserConfigStore();
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
-
- const userMeConfig = userConfig.config;
-
- const [refreshVideoList, setRefreshVideoList] = useState(false);
-
- const [videoResponse, setVideoReponse] = useState();
- const [continueVideoResponse, setContinueVideoResponse] =
- useState();
-
- const videoList = videoResponse?.data;
- const pagination = videoResponse?.paginate;
- const continueVideos = continueVideoResponse?.data;
-
- const hasVideos = videoResponse?.data?.length !== 0;
- const showEmbeddedVideo = videoId !== null;
-
- const isGridView = userMeConfig.view_style_home === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${userMeConfig.grid_items}` : '';
- const gridViewGrid = isGridView ? `grid-${userMeConfig.grid_items}` : '';
-
- useEffect(() => {
- (async () => {
- if (
- refreshVideoList ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const videos = await loadVideoListByFilter({
- page: currentPage,
- watch: userMeConfig.hide_watched ? 'unwatched' : undefined,
- sort: userMeConfig.sort_by,
- order: userMeConfig.sort_order,
- });
-
- try {
- const continueVideoResponse = await loadVideoListByFilter({ watch: 'continue' });
- setContinueVideoResponse(continueVideoResponse);
- } catch (error) {
- console.log('Server error on continue vids?');
- }
-
- setVideoReponse(videos);
-
- setRefreshVideoList(false);
- }
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- refreshVideoList,
- userMeConfig.sort_by,
- userMeConfig.sort_order,
- userMeConfig.hide_watched,
- currentPage,
- pagination?.current_page
- ]);
-
- return (
- <>
- TubeArchivist
-
-
- {showEmbeddedVideo && }
-
-
- {continueVideos && continueVideos.length > 0 && (
- <>
-
-
Continue Watching
-
-
-
-
- >
- )}
-
-
-
Recent Videos
-
-
-
-
-
-
-
- {!hasVideos && (
- <>
-
No videos found...
-
- If you've already added a channel or playlist, try going to the{' '}
- downloads page to start the scan and download
- tasks.
-
- >
- )}
-
- {hasVideos && (
-
- )}
-
-
-
- {pagination && (
-
- )}
- >
- );
-};
-
-export default Home;
+import { useEffect, useState } from 'react';
+import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
+import Routes from '../configuration/routes/RouteList';
+import Pagination from '../components/Pagination';
+import loadVideoListByFilter, {
+ VideoListByFilterResponseType,
+} from '../api/loader/loadVideoListByPage';
+import VideoList from '../components/VideoList';
+import { ChannelType } from './Channels';
+import { OutletContextType } from './Base';
+import Filterbar from '../components/Filterbar';
+import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import { SponsorBlockType } from './Video';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+export type PlayerType = {
+ watched: boolean;
+ duration: number;
+ duration_str: string;
+ progress: number;
+ position: number;
+};
+
+export type StatsType = {
+ view_count: number;
+ like_count: number;
+ dislike_count: number;
+ average_rating: number;
+};
+
+export type StreamType = {
+ type: string;
+ index: number;
+ codec: string;
+ width?: number;
+ height?: number;
+ bitrate: number;
+};
+
+export type Subtitles = {
+ ext: string;
+ url: string;
+ name: string;
+ lang: string;
+ source: string;
+ media_url: string;
+};
+
+export type VideoType = {
+ active: boolean;
+ category: string[];
+ channel: ChannelType;
+ date_downloaded: number;
+ description: string;
+ comment_count?: number;
+ media_size: number;
+ media_url: string;
+ player: PlayerType;
+ published: string;
+ sponsorblock?: SponsorBlockType;
+ playlist?: string[];
+ stats: StatsType;
+ streams: StreamType[];
+ subtitles: Subtitles[];
+ tags: string[];
+ title: string;
+ vid_last_refresh: string;
+ vid_thumb_base64: boolean;
+ vid_thumb_url: string;
+ vid_type: string;
+ youtube_id: string;
+};
+
+export type DownloadsType = {
+ limit_speed: boolean;
+ sleep_interval: number;
+ autodelete_days: boolean;
+ format: boolean;
+ format_sort: boolean;
+ add_metadata: boolean;
+ add_thumbnail: boolean;
+ subtitle: boolean;
+ subtitle_source: boolean;
+ subtitle_index: boolean;
+ comment_max: boolean;
+ comment_sort: string;
+ cookie_import: boolean;
+ throttledratelimit: boolean;
+ extractor_lang: boolean;
+ integrate_ryd: boolean;
+ integrate_sponsorblock: boolean;
+};
+
+export type ConfigType = {
+ enable_cast: boolean;
+ downloads: DownloadsType;
+};
+
+export type SortByType = 'published' | 'downloaded' | 'views' | 'likes' | 'duration' | 'mediasize';
+export type SortOrderType = 'asc' | 'desc';
+export type ViewLayoutType = 'grid' | 'list';
+
+const Home = () => {
+ const { userConfig } = useUserConfigStore();
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+
+ const userMeConfig = userConfig.config;
+
+ const [refreshVideoList, setRefreshVideoList] = useState(false);
+
+ const [videoResponse, setVideoReponse] = useState();
+ const [continueVideoResponse, setContinueVideoResponse] =
+ useState();
+
+ const videoList = videoResponse?.data;
+ const pagination = videoResponse?.paginate;
+ const continueVideos = continueVideoResponse?.data;
+
+ const hasVideos = videoResponse?.data?.length !== 0;
+ const showEmbeddedVideo = videoId !== null;
+
+ const isGridView = userMeConfig.view_style_home === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${userMeConfig.grid_items}` : '';
+ const gridViewGrid = isGridView ? `grid-${userMeConfig.grid_items}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refreshVideoList ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const videos = await loadVideoListByFilter({
+ page: currentPage,
+ watch: userMeConfig.hide_watched ? 'unwatched' : undefined,
+ sort: userMeConfig.sort_by,
+ order: userMeConfig.sort_order,
+ });
+
+ try {
+ const continueVideoResponse = await loadVideoListByFilter({ watch: 'continue' });
+ setContinueVideoResponse(continueVideoResponse);
+ } catch (error) {
+ console.log('Server error on continue vids?');
+ }
+
+ setVideoReponse(videos);
+
+ setRefreshVideoList(false);
+ }
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ refreshVideoList,
+ userMeConfig.sort_by,
+ userMeConfig.sort_order,
+ userMeConfig.hide_watched,
+ currentPage,
+ pagination?.current_page,
+ ]);
+
+ return (
+ <>
+ TubeArchivist
+
+
+ {showEmbeddedVideo && }
+
+
+ {continueVideos && continueVideos.length > 0 && (
+ <>
+
+
Continue Watching
+
+
+
+
+ >
+ )}
+
+
+
Recent Videos
+
+
+
+
+
+
+
+ {!hasVideos && (
+ <>
+
No videos found...
+
+ If you've already added a channel or playlist, try going to the{' '}
+ downloads page to start the scan and download
+ tasks.
+
+ >
+ )}
+
+ {hasVideos && (
+
+ )}
+
+
+
+ {pagination && (
+
+ )}
+ >
+ );
+};
+
+export default Home;
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index e3d8e45f..43a211e8 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -1,103 +1,103 @@
-import { useState } from 'react';
-import Routes from '../configuration/routes/RouteList';
-import { useNavigate } from 'react-router-dom';
-import importColours from '../configuration/colours/getColours';
-import Button from '../components/Button';
-import signIn from '../api/actions/signIn';
-
-const Login = () => {
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [saveLogin, setSaveLogin] = useState(false);
- const navigate = useNavigate();
-
- importColours();
-
- const form_error = false;
-
- const handleSubmit = async (event: { preventDefault: () => void }) => {
- event.preventDefault();
-
- const loginResponse = await signIn(username, password, saveLogin);
-
- const signedIn = loginResponse.status === 200;
-
- if (signedIn) {
- navigate(Routes.Home);
- } else {
- navigate(Routes.Login);
- }
- };
-
- return (
- <>
- TA | Welcome
-
-
-
Tube Archivist
-
Your Self Hosted YouTube Media Server
-
- {form_error &&
Failed to login.
}
-
-
-
-
-
- Github
-
- {' '}
-
-
- Donate
-
-
-
-
-
- >
- );
-};
-
-export default Login;
+import { useState } from 'react';
+import Routes from '../configuration/routes/RouteList';
+import { useNavigate } from 'react-router-dom';
+import importColours from '../configuration/colours/getColours';
+import Button from '../components/Button';
+import signIn from '../api/actions/signIn';
+
+const Login = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [saveLogin, setSaveLogin] = useState(false);
+ const navigate = useNavigate();
+
+ importColours();
+
+ const form_error = false;
+
+ const handleSubmit = async (event: { preventDefault: () => void }) => {
+ event.preventDefault();
+
+ const loginResponse = await signIn(username, password, saveLogin);
+
+ const signedIn = loginResponse.status === 200;
+
+ if (signedIn) {
+ navigate(Routes.Home);
+ } else {
+ navigate(Routes.Login);
+ }
+ };
+
+ return (
+ <>
+ TA | Welcome
+
+
+
Tube Archivist
+
Your Self Hosted YouTube Media Server
+
+ {form_error &&
Failed to login.
}
+
+
+
+
+
+ Github
+
+ {' '}
+
+
+ Donate
+
+
+
+
+
+ >
+ );
+};
+
+export default Login;
diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx
index 06245871..0365b96e 100644
--- a/frontend/src/pages/Playlist.tsx
+++ b/frontend/src/pages/Playlist.tsx
@@ -1,385 +1,379 @@
-import { useEffect, useState } from 'react';
-import {
- Link,
- useNavigate,
- useOutletContext,
- useParams,
- useSearchParams,
-} from 'react-router-dom';
-import loadPlaylistById from '../api/loader/loadPlaylistById';
-import { OutletContextType } from './Base';
-import { ConfigType, VideoType, ViewLayoutType } from './Home';
-import Filterbar from '../components/Filterbar';
-import { PlaylistEntryType } from './Playlists';
-import loadChannelById from '../api/loader/loadChannelById';
-import VideoList from '../components/VideoList';
-import Pagination, { PaginationType } from '../components/Pagination';
-import ChannelOverview from '../components/ChannelOverview';
-import Linkify from '../components/Linkify';
-import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
-import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
-import deletePlaylist from '../api/actions/deletePlaylist';
-import Routes from '../configuration/routes/RouteList';
-import { ChannelResponseType } from './ChannelBase';
-import formatDate from '../functions/formatDates';
-import queueReindex from '../api/actions/queueReindex';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import Button from '../components/Button';
-import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
-import loadIsAdmin from '../functions/getIsAdmin';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-export type PlaylistType = {
- playlist_active: boolean;
- playlist_channel: string;
- playlist_channel_id: string;
- playlist_description: string;
- playlist_entries: PlaylistEntryType[];
- playlist_id: string;
- playlist_last_refresh: string;
- playlist_name: string;
- playlist_subscribed: boolean;
- playlist_thumbnail: string;
- playlist_type: string;
- _index: string;
- _score: number;
-};
-
-export type PlaylistResponseType = {
- data?: PlaylistType;
- config?: ConfigType;
-};
-
-export type VideoResponseType = {
- data?: VideoType[];
- config?: ConfigType;
- paginate?: PaginationType;
-};
-
-const Playlist = () => {
- const { playlistId } = useParams();
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
-
- const { userConfig } = useUserConfigStore();
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const isAdmin = loadIsAdmin();
-
- const userMeConfig = userConfig.config;
-
- const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
- const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
- const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
- const [refresh, setRefresh] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [reindex, setReindex] = useState(false);
-
- const [playlistResponse, setPlaylistResponse] = useState();
- const [channelResponse, setChannelResponse] = useState();
- const [videoResponse, setVideoResponse] = useState();
-
- const playlist = playlistResponse?.data;
- const channel = channelResponse?.data;
- const videos = videoResponse?.data;
- const pagination = videoResponse?.paginate;
-
- const palylistEntries = playlistResponse?.data?.playlist_entries;
- const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
- const videoInPlaylistCount = pagination?.total_hits;
- const showEmbeddedVideo = videoId !== null;
-
- const isGridView = view === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- if (
- refresh ||
- pagination?.current_page === undefined ||
- currentPage !== pagination?.current_page
- ) {
- const playlist = await loadPlaylistById(playlistId);
- const video = await loadVideoListByFilter({
- playlist: playlistId,
- page: currentPage,
- watch: hideWatched ? 'unwatched' : undefined,
- sort: 'downloaded', // downloaded or published? or playlist sort order?
- });
-
- const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
- if (!isCustomPlaylist) {
- const channel = await loadChannelById(playlist.data.playlist_channel_id);
-
- setChannelResponse(channel);
- }
-
- setPlaylistResponse(playlist);
- setVideoResponse(video);
- setRefresh(false);
- }
- })();
- // Do not add hideWatched this will not work as expected!
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [playlistId, refresh, currentPage, pagination?.current_page]);
-
- if (!playlistId || !playlist) {
- return `Playlist ${playlistId} not found!`;
- }
-
- const isCustomPlaylist = playlist.playlist_type === 'custom';
-
- return (
- <>
- {`TA | Playlist: ${playlist.playlist_name}`}
-
-
-
-
{playlist.playlist_name}
-
-
- {!isCustomPlaylist && channel && (
-
- )}
-
-
-
-
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
- {!isCustomPlaylist && (
- <>
-
- Playlist:
- {playlist.playlist_subscribed && (
- <>
- {isAdmin && (
- {
- await updatePlaylistSubscription(playlistId, false);
-
- setRefresh(true);
- }}
- />
- )}
- >
- )}{' '}
- {!playlist.playlist_subscribed && (
- {
- await updatePlaylistSubscription(playlistId, true);
-
- setRefresh(true);
- }}
- />
- )}
-
- {playlist.playlist_active && (
-
- Youtube:{' '}
-
- Active
-
-
- )}
- {!playlist.playlist_active &&
Youtube: Deactivated
}
- >
- )}
-
- {!showDeleteConfirm && (
-
setShowDeleteConfirm(!showDeleteConfirm)}
- />
- )}
-
- {showDeleteConfirm && (
-
- Delete {playlist.playlist_name}?
-
- {
- await deletePlaylist(playlistId, false);
- navigate(Routes.Playlists);
- }}
- />
-
- {
- await deletePlaylist(playlistId, true);
- navigate(Routes.Playlists);
- }}
- />
-
-
- setShowDeleteConfirm(!showDeleteConfirm)} />
-
- )}
-
-
-
-
- {videoArchivedCount > 0 && (
- <>
-
- Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
-
-
- {
- await updateWatchedState({
- id: playlistId,
- is_watched: true,
- });
-
- setRefresh(true);
- }}
- />{' '}
- {
- await updateWatchedState({
- id: playlistId,
- is_watched: false,
- });
-
- setRefresh(true);
- }}
- />
-
- >
- )}
-
- {reindex &&
Reindex scheduled
}
- {!reindex && (
-
- {!isCustomPlaylist && (
- {
- setReindex(true);
-
- await queueReindex(playlist.playlist_id, 'playlist');
- }}
- />
- )}{' '}
- {
- setReindex(true);
-
- await queueReindex(playlist.playlist_id, 'playlist', true);
- }}
- />
-
- )}
-
-
-
-
- {playlist.playlist_description && (
-
-
- {playlist.playlist_description}
-
-
-
setDescriptionExpanded(!descriptionExpanded)}
- />
-
- )}
-
-
-
-
-
-
- {showEmbeddedVideo && }
-
-
-
- {videoInPlaylistCount === 0 && (
- <>
-
No videos found...
- {isCustomPlaylist && (
-
- Try going to the home page to add videos to this
- playlist.
-
- )}
-
- {!isCustomPlaylist && (
-
- Try going to the downloads page to start the
- scan and download tasks.
-
- )}
- >
- )}
- {videoInPlaylistCount !== 0 && (
-
- )}
-
-
-
-
- >
- );
-};
-
-export default Playlist;
+import { useEffect, useState } from 'react';
+import { Link, useNavigate, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
+import loadPlaylistById from '../api/loader/loadPlaylistById';
+import { OutletContextType } from './Base';
+import { ConfigType, VideoType, ViewLayoutType } from './Home';
+import Filterbar from '../components/Filterbar';
+import { PlaylistEntryType } from './Playlists';
+import loadChannelById from '../api/loader/loadChannelById';
+import VideoList from '../components/VideoList';
+import Pagination, { PaginationType } from '../components/Pagination';
+import ChannelOverview from '../components/ChannelOverview';
+import Linkify from '../components/Linkify';
+import { ViewStyleNames, ViewStyles } from '../configuration/constants/ViewStyle';
+import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
+import deletePlaylist from '../api/actions/deletePlaylist';
+import Routes from '../configuration/routes/RouteList';
+import { ChannelResponseType } from './ChannelBase';
+import formatDate from '../functions/formatDates';
+import queueReindex from '../api/actions/queueReindex';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import Button from '../components/Button';
+import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
+import loadIsAdmin from '../functions/getIsAdmin';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+export type PlaylistType = {
+ playlist_active: boolean;
+ playlist_channel: string;
+ playlist_channel_id: string;
+ playlist_description: string;
+ playlist_entries: PlaylistEntryType[];
+ playlist_id: string;
+ playlist_last_refresh: string;
+ playlist_name: string;
+ playlist_subscribed: boolean;
+ playlist_thumbnail: string;
+ playlist_type: string;
+ _index: string;
+ _score: number;
+};
+
+export type PlaylistResponseType = {
+ data?: PlaylistType;
+ config?: ConfigType;
+};
+
+export type VideoResponseType = {
+ data?: VideoType[];
+ config?: ConfigType;
+ paginate?: PaginationType;
+};
+
+const Playlist = () => {
+ const { playlistId } = useParams();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+
+ const { userConfig } = useUserConfigStore();
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const isAdmin = loadIsAdmin();
+
+ const userMeConfig = userConfig.config;
+
+ const [hideWatched, setHideWatched] = useState(userMeConfig.hide_watched || false);
+ const [view, setView] = useState(userMeConfig.view_style_home || 'grid');
+ const [gridItems, setGridItems] = useState(userMeConfig.grid_items || 3);
+ const [descriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [refresh, setRefresh] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [reindex, setReindex] = useState(false);
+
+ const [playlistResponse, setPlaylistResponse] = useState();
+ const [channelResponse, setChannelResponse] = useState();
+ const [videoResponse, setVideoResponse] = useState();
+
+ const playlist = playlistResponse?.data;
+ const channel = channelResponse?.data;
+ const videos = videoResponse?.data;
+ const pagination = videoResponse?.paginate;
+
+ const palylistEntries = playlistResponse?.data?.playlist_entries;
+ const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
+ const videoInPlaylistCount = pagination?.total_hits;
+ const showEmbeddedVideo = videoId !== null;
+
+ const isGridView = view === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (
+ refresh ||
+ pagination?.current_page === undefined ||
+ currentPage !== pagination?.current_page
+ ) {
+ const playlist = await loadPlaylistById(playlistId);
+ const video = await loadVideoListByFilter({
+ playlist: playlistId,
+ page: currentPage,
+ watch: hideWatched ? 'unwatched' : undefined,
+ sort: 'downloaded', // downloaded or published? or playlist sort order?
+ });
+
+ const isCustomPlaylist = playlist?.data?.playlist_type === 'custom';
+ if (!isCustomPlaylist) {
+ const channel = await loadChannelById(playlist.data.playlist_channel_id);
+
+ setChannelResponse(channel);
+ }
+
+ setPlaylistResponse(playlist);
+ setVideoResponse(video);
+ setRefresh(false);
+ }
+ })();
+ // Do not add hideWatched this will not work as expected!
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [playlistId, refresh, currentPage, pagination?.current_page]);
+
+ if (!playlistId || !playlist) {
+ return `Playlist ${playlistId} not found!`;
+ }
+
+ const isCustomPlaylist = playlist.playlist_type === 'custom';
+
+ return (
+ <>
+ {`TA | Playlist: ${playlist.playlist_name}`}
+
+
+
+
{playlist.playlist_name}
+
+
+ {!isCustomPlaylist && channel && (
+
+ )}
+
+
+
+
Last refreshed: {formatDate(playlist.playlist_last_refresh)}
+ {!isCustomPlaylist && (
+ <>
+
+ Playlist:
+ {playlist.playlist_subscribed && (
+ <>
+ {isAdmin && (
+ {
+ await updatePlaylistSubscription(playlistId, false);
+
+ setRefresh(true);
+ }}
+ />
+ )}
+ >
+ )}{' '}
+ {!playlist.playlist_subscribed && (
+ {
+ await updatePlaylistSubscription(playlistId, true);
+
+ setRefresh(true);
+ }}
+ />
+ )}
+
+ {playlist.playlist_active && (
+
+ Youtube:{' '}
+
+ Active
+
+
+ )}
+ {!playlist.playlist_active &&
Youtube: Deactivated
}
+ >
+ )}
+
+ {!showDeleteConfirm && (
+
setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+ )}
+
+ {showDeleteConfirm && (
+
+ Delete {playlist.playlist_name}?
+
+ {
+ await deletePlaylist(playlistId, false);
+ navigate(Routes.Playlists);
+ }}
+ />
+
+ {
+ await deletePlaylist(playlistId, true);
+ navigate(Routes.Playlists);
+ }}
+ />
+
+
+ setShowDeleteConfirm(!showDeleteConfirm)} />
+
+ )}
+
+
+
+
+ {videoArchivedCount > 0 && (
+ <>
+
+ Total Videos archived: {videoArchivedCount}/{videoInPlaylistCount}
+
+
+ {
+ await updateWatchedState({
+ id: playlistId,
+ is_watched: true,
+ });
+
+ setRefresh(true);
+ }}
+ />{' '}
+ {
+ await updateWatchedState({
+ id: playlistId,
+ is_watched: false,
+ });
+
+ setRefresh(true);
+ }}
+ />
+
+ >
+ )}
+
+ {reindex &&
Reindex scheduled
}
+ {!reindex && (
+
+ {!isCustomPlaylist && (
+ {
+ setReindex(true);
+
+ await queueReindex(playlist.playlist_id, 'playlist');
+ }}
+ />
+ )}{' '}
+ {
+ setReindex(true);
+
+ await queueReindex(playlist.playlist_id, 'playlist', true);
+ }}
+ />
+
+ )}
+
+
+
+
+ {playlist.playlist_description && (
+
+
+ {playlist.playlist_description}
+
+
+
setDescriptionExpanded(!descriptionExpanded)}
+ />
+
+ )}
+
+
+
+
+
+
+ {showEmbeddedVideo && }
+
+
+
+ {videoInPlaylistCount === 0 && (
+ <>
+
No videos found...
+ {isCustomPlaylist && (
+
+ Try going to the home page to add videos to this
+ playlist.
+
+ )}
+
+ {!isCustomPlaylist && (
+
+ Try going to the downloads page to start the
+ scan and download tasks.
+
+ )}
+ >
+ )}
+ {videoInPlaylistCount !== 0 && (
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Playlist;
diff --git a/frontend/src/pages/Playlists.tsx b/frontend/src/pages/Playlists.tsx
index a93c320e..adcff299 100644
--- a/frontend/src/pages/Playlists.tsx
+++ b/frontend/src/pages/Playlists.tsx
@@ -1,196 +1,194 @@
-import { useEffect, useState } from 'react';
-import { useOutletContext } from 'react-router-dom';
-
-import iconAdd from '/img/icon-add.svg';
-import iconGridView from '/img/icon-gridview.svg';
-import iconListView from '/img/icon-listview.svg';
-
-import { OutletContextType } from './Base';
-import loadPlaylistList from '../api/loader/loadPlaylistList';
-import { ConfigType } from './Home';
-import Pagination, { PaginationType } from '../components/Pagination';
-import PlaylistList from '../components/PlaylistList';
-import { PlaylistType } from './Playlist';
-import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
-import createCustomPlaylist from '../api/actions/createCustomPlaylist';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import Button from '../components/Button';
-import loadIsAdmin from '../functions/getIsAdmin';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-export type PlaylistEntryType = {
- youtube_id: string;
- title: string;
- uploader: string;
- idx: number;
- downloaded: boolean;
-};
-
-export type PlaylistsResponseType = {
- data?: PlaylistType[];
- config?: ConfigType;
- paginate?: PaginationType;
-};
-
-const Playlists = () => {
- const { userConfig, setPartialConfig } = useUserConfigStore();
- const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
- const isAdmin = loadIsAdmin();
-
- const [showAddForm, setShowAddForm] = useState(false);
- const [refresh, setRefresh] = useState(false);
- const [playlistsToAddText, setPlaylistsToAddText] = useState('');
- const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
-
- const [playlistResponse, setPlaylistReponse] = useState();
-
- const playlistList = playlistResponse?.data;
- const pagination = playlistResponse?.paginate;
-
- const hasPlaylists = playlistResponse?.data?.length !== 0;
-
- const view = userConfig.config.view_style_playlist;
- const showSubedOnly = userConfig.config.show_subed_only;
-
- useEffect(() => {
- (async () => {
- const playlist = await loadPlaylistList({
- page: currentPage,
- subscribed: showSubedOnly,
- });
-
- setPlaylistReponse(playlist);
- setRefresh(false);
- })();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
-
- return (
- <>
- TA | Playlists
-
-
-
-
-
Playlists
-
- {isAdmin && (
-
-
{
- setShowAddForm(!showAddForm);
- }}
- src={iconAdd}
- alt="add-icon"
- title="Subscribe to Playlists"
- />
- {showAddForm && (
-
-
- Subscribe to playlists:
-
-
-
- Or create custom playlist:
-
-
- )}
-
- )}
-
-
-
-
-
-
-
Show subscribed only:
-
- {
- setPartialConfig({show_subed_only: !showSubedOnly});
- }}
- type="checkbox"
- />
- {!showSubedOnly && (
-
- Off
-
- )}
- {showSubedOnly && (
-
- On
-
- )}
-
-
-
-
{
- setPartialConfig({view_style_playlist: 'grid'});
- }}
- alt="grid view"
- />
-
{
- setPartialConfig({view_style_playlist: 'list'});
- }}
- alt="list view"
- />
-
-
-
-
- {!hasPlaylists &&
No playlists found... }
-
- {hasPlaylists && (
-
- )}
-
-
-
-
- >
- );
-};
-
-export default Playlists;
+import { useEffect, useState } from 'react';
+import { useOutletContext } from 'react-router-dom';
+
+import iconAdd from '/img/icon-add.svg';
+import iconGridView from '/img/icon-gridview.svg';
+import iconListView from '/img/icon-listview.svg';
+
+import { OutletContextType } from './Base';
+import loadPlaylistList from '../api/loader/loadPlaylistList';
+import { ConfigType } from './Home';
+import Pagination, { PaginationType } from '../components/Pagination';
+import PlaylistList from '../components/PlaylistList';
+import { PlaylistType } from './Playlist';
+import updatePlaylistSubscription from '../api/actions/updatePlaylistSubscription';
+import createCustomPlaylist from '../api/actions/createCustomPlaylist';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import Button from '../components/Button';
+import loadIsAdmin from '../functions/getIsAdmin';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+export type PlaylistEntryType = {
+ youtube_id: string;
+ title: string;
+ uploader: string;
+ idx: number;
+ downloaded: boolean;
+};
+
+export type PlaylistsResponseType = {
+ data?: PlaylistType[];
+ config?: ConfigType;
+ paginate?: PaginationType;
+};
+
+const Playlists = () => {
+ const { userConfig, setPartialConfig } = useUserConfigStore();
+ const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
+ const isAdmin = loadIsAdmin();
+
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [refresh, setRefresh] = useState(false);
+ const [playlistsToAddText, setPlaylistsToAddText] = useState('');
+ const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
+
+ const [playlistResponse, setPlaylistReponse] = useState();
+
+ const playlistList = playlistResponse?.data;
+ const pagination = playlistResponse?.paginate;
+
+ const hasPlaylists = playlistResponse?.data?.length !== 0;
+
+ const view = userConfig.config.view_style_playlist;
+ const showSubedOnly = userConfig.config.show_subed_only;
+
+ useEffect(() => {
+ (async () => {
+ const playlist = await loadPlaylistList({
+ page: currentPage,
+ subscribed: showSubedOnly,
+ });
+
+ setPlaylistReponse(playlist);
+ setRefresh(false);
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [refresh, userConfig.config.show_subed_only, currentPage, pagination?.current_page]);
+
+ return (
+ <>
+ TA | Playlists
+
+
+
+
+
Playlists
+
+ {isAdmin && (
+
+
{
+ setShowAddForm(!showAddForm);
+ }}
+ src={iconAdd}
+ alt="add-icon"
+ title="Subscribe to Playlists"
+ />
+ {showAddForm && (
+
+
+ Subscribe to playlists:
+
+
+
+ Or create custom playlist:
+
+
+ )}
+
+ )}
+
+
+
+
+
+
+
Show subscribed only:
+
+ {
+ setPartialConfig({ show_subed_only: !showSubedOnly });
+ }}
+ type="checkbox"
+ />
+ {!showSubedOnly && (
+
+ Off
+
+ )}
+ {showSubedOnly && (
+
+ On
+
+ )}
+
+
+
+
{
+ setPartialConfig({ view_style_playlist: 'grid' });
+ }}
+ alt="grid view"
+ />
+
{
+ setPartialConfig({ view_style_playlist: 'list' });
+ }}
+ alt="list view"
+ />
+
+
+
+
+ {!hasPlaylists &&
No playlists found... }
+
+ {hasPlaylists &&
}
+
+
+
+
+ >
+ );
+};
+
+export default Playlists;
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx
index 463d2484..77399aca 100644
--- a/frontend/src/pages/Search.tsx
+++ b/frontend/src/pages/Search.tsx
@@ -1,163 +1,164 @@
-import { useSearchParams } from 'react-router-dom';
-import { useEffect, useState } from 'react';
-import { VideoType } from './Home';
-import loadSearch from '../api/loader/loadSearch';
-import { PlaylistType } from './Playlist';
-import { ChannelType } from './Channels';
-import VideoList from '../components/VideoList';
-import ChannelList from '../components/ChannelList';
-import PlaylistList from '../components/PlaylistList';
-import SubtitleList from '../components/SubtitleList';
-import { ViewStyles } from '../configuration/constants/ViewStyle';
-import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
-import SearchExampleQueries from '../components/SearchExampleQueries';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-
-const EmptySearchResponse: SearchResultsType = {
- results: {
- video_results: [],
- channel_results: [],
- playlist_results: [],
- fulltext_results: [],
- },
- queryType: 'simple',
-};
-
-type SearchResultType = {
- video_results: VideoType[];
- channel_results: ChannelType[];
- playlist_results: PlaylistType[];
- fulltext_results: [];
-};
-
-type SearchResultsType = {
- results: SearchResultType;
- queryType: string;
-};
-
-const Search = () => {
- const { userConfig } = useUserConfigStore();
- const [searchParams] = useSearchParams();
- const videoId = searchParams.get('videoId');
- const userMeConfig = userConfig.config;
-
- const viewVideos = userMeConfig.view_style_home;
- const viewChannels = userMeConfig.view_style_channel;
- const viewPlaylists = userMeConfig.view_style_playlist;
- const gridItems = userMeConfig.grid_items || 3;
-
- const [searchQuery, setSearchQuery] = useState('');
- const [searchResults, setSearchResults] = useState();
-
- const [refresh, setRefresh] = useState(false);
-
- const videoList = searchResults?.results.video_results;
- const channelList = searchResults?.results.channel_results;
- const playlistList = searchResults?.results.playlist_results;
- const fulltextList = searchResults?.results.fulltext_results;
- const queryType = searchResults?.queryType;
- const showEmbeddedVideo = videoId !== null;
-
- const hasSearchQuery = searchQuery.length > 0;
- const hasVideos = Number(videoList?.length) > 0;
- const hasChannels = Number(channelList?.length) > 0;
- const hasPlaylist = Number(playlistList?.length) > 0;
- const hasFulltext = Number(fulltextList?.length) > 0;
-
- const isSimpleQuery = queryType === 'simple';
- const isVideoQuery = queryType === 'video' || isSimpleQuery;
- const isChannelQuery = queryType === 'channel' || isSimpleQuery;
- const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
- const isFullTextQuery = queryType === 'full' || isSimpleQuery;
-
- const isGridView = viewVideos === ViewStyles.grid;
- const gridView = isGridView ? `boxed-${gridItems}` : '';
- const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
-
- useEffect(() => {
- (async () => {
- if (!hasSearchQuery) {
- setSearchResults(EmptySearchResponse);
-
- return;
- }
-
- const searchResults = await loadSearch(searchQuery);
-
- setSearchResults(searchResults);
- setRefresh(false);
- })();
- }, [searchQuery, refresh, hasSearchQuery]);
-
- return (
- <>
- TubeArchivist
- {showEmbeddedVideo && }
-
-
-
Search your Archive
-
-
-
- {
- setSearchQuery(event.target.value);
- }}
- />
-
-
-
- {hasSearchQuery && isVideoQuery && (
-
- )}
-
- {hasSearchQuery && isChannelQuery && (
-
-
Channel Results
-
-
-
-
- )}
-
- {hasSearchQuery && isPlaylistQuery && (
-
- )}
-
- {hasSearchQuery && isFullTextQuery && (
-
-
Fulltext Results
-
-
-
-
- )}
-
-
- {!hasVideos && !hasChannels && !hasPlaylist && !hasFulltext &&
}
-
- >
- );
-};
-
-export default Search;
+import { useSearchParams } from 'react-router-dom';
+import { useEffect, useState } from 'react';
+import { VideoType } from './Home';
+import loadSearch from '../api/loader/loadSearch';
+import { PlaylistType } from './Playlist';
+import { ChannelType } from './Channels';
+import VideoList from '../components/VideoList';
+import ChannelList from '../components/ChannelList';
+import PlaylistList from '../components/PlaylistList';
+import SubtitleList from '../components/SubtitleList';
+import { ViewStyles } from '../configuration/constants/ViewStyle';
+import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
+import SearchExampleQueries from '../components/SearchExampleQueries';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+
+const EmptySearchResponse: SearchResultsType = {
+ results: {
+ video_results: [],
+ channel_results: [],
+ playlist_results: [],
+ fulltext_results: [],
+ },
+ queryType: 'simple',
+};
+
+type SearchResultType = {
+ video_results: VideoType[];
+ channel_results: ChannelType[];
+ playlist_results: PlaylistType[];
+ fulltext_results: [];
+};
+
+type SearchResultsType = {
+ results: SearchResultType;
+ queryType: string;
+};
+
+const Search = () => {
+ const { userConfig } = useUserConfigStore();
+ const [searchParams] = useSearchParams();
+ const videoId = searchParams.get('videoId');
+ const userMeConfig = userConfig.config;
+
+ const viewVideos = userMeConfig.view_style_home;
+ const viewChannels = userMeConfig.view_style_channel;
+ const viewPlaylists = userMeConfig.view_style_playlist;
+ const gridItems = userMeConfig.grid_items || 3;
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState();
+
+ const [refresh, setRefresh] = useState(false);
+
+ const videoList = searchResults?.results.video_results;
+ const channelList = searchResults?.results.channel_results;
+ const playlistList = searchResults?.results.playlist_results;
+ const fulltextList = searchResults?.results.fulltext_results;
+ const queryType = searchResults?.queryType;
+ const showEmbeddedVideo = videoId !== null;
+
+ const hasSearchQuery = searchQuery.length > 0;
+ const hasVideos = Number(videoList?.length) > 0;
+ const hasChannels = Number(channelList?.length) > 0;
+ const hasPlaylist = Number(playlistList?.length) > 0;
+ const hasFulltext = Number(fulltextList?.length) > 0;
+
+ const isSimpleQuery = queryType === 'simple';
+ const isVideoQuery = queryType === 'video' || isSimpleQuery;
+ const isChannelQuery = queryType === 'channel' || isSimpleQuery;
+ const isPlaylistQuery = queryType === 'playlist' || isSimpleQuery;
+ const isFullTextQuery = queryType === 'full' || isSimpleQuery;
+
+ const isGridView = viewVideos === ViewStyles.grid;
+ const gridView = isGridView ? `boxed-${gridItems}` : '';
+ const gridViewGrid = isGridView ? `grid-${gridItems}` : '';
+
+ useEffect(() => {
+ (async () => {
+ if (!hasSearchQuery) {
+ setSearchResults(EmptySearchResponse);
+
+ return;
+ }
+
+ const searchResults = await loadSearch(searchQuery);
+
+ setSearchResults(searchResults);
+ setRefresh(false);
+ })();
+ }, [searchQuery, refresh, hasSearchQuery]);
+
+ return (
+ <>
+ TubeArchivist
+ {showEmbeddedVideo && }
+
+
+
Search your Archive
+
+
+
+ {
+ setSearchQuery(event.target.value);
+ }}
+ />
+
+
+
+ {hasSearchQuery && isVideoQuery && (
+
+ )}
+
+ {hasSearchQuery && isChannelQuery && (
+
+
Channel Results
+
+
+
+
+ )}
+
+ {hasSearchQuery && isPlaylistQuery && (
+
+ )}
+
+ {hasSearchQuery && isFullTextQuery && (
+
+
Fulltext Results
+
+
+
+
+ )}
+
+
+ {!hasVideos && !hasChannels && !hasPlaylist && !hasFulltext &&
}
+
+ >
+ );
+};
+
+export default Search;
diff --git a/frontend/src/pages/SettingsActions.tsx b/frontend/src/pages/SettingsActions.tsx
index a3321a4a..9bb532ab 100644
--- a/frontend/src/pages/SettingsActions.tsx
+++ b/frontend/src/pages/SettingsActions.tsx
@@ -1,242 +1,242 @@
-import { useEffect, useState } from 'react';
-import loadBackupList from '../api/loader/loadBackupList';
-import SettingsNavigation from '../components/SettingsNavigation';
-import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
-import updateTaskByName from '../api/actions/updateTaskByName';
-import queueBackup from '../api/actions/queueBackup';
-import restoreBackup from '../api/actions/restoreBackup';
-import Notifications from '../components/Notifications';
-import Button from '../components/Button';
-
-type Backup = {
- filename: string;
- file_path: string;
- file_size: number;
- timestamp: string;
- reason: string;
-};
-
-type BackupListType = Backup[];
-
-const SettingsActions = () => {
- const [deleteIgnored, setDeleteIgnored] = useState(false);
- const [deletePending, setDeletePending] = useState(false);
- const [processingImports, setProcessingImports] = useState(false);
- const [reEmbed, setReEmbed] = useState(false);
- const [backupStarted, setBackupStarted] = useState(false);
- const [isRestoringBackup, setIsRestoringBackup] = useState(false);
- const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
-
- const [backupListResponse, setBackupListResponse] = useState();
-
- const backups = backupListResponse;
- const hasBackups = !!backups && backups?.length > 0;
-
- useEffect(() => {
- (async () => {
- const backupListResponse = await loadBackupList();
-
- setBackupListResponse(backupListResponse);
- })();
- }, []);
-
- return (
- <>
- TA | Actions
-
-
-
{
- setDeleteIgnored(false);
- setDeletePending(false);
- setProcessingImports(false);
- setReEmbed(false);
- setBackupStarted(false);
- setIsRestoringBackup(false);
- setReScanningFileSystem(false);
- }}
- />
-
-
-
Actions
-
-
-
Delete download queue
-
Delete your pending or previously ignored videos from your download queue.
- {deleteIgnored &&
Deleting download queue: ignored
}
- {!deleteIgnored && (
-
{
- await deleteDownloadQueueByFilter('ignore');
- setDeleteIgnored(true);
- }}
- />
- )}{' '}
- {deletePending && Deleting download queue: pending
}
- {!deletePending && (
- {
- await deleteDownloadQueueByFilter('pending');
- setDeletePending(true);
- }}
- />
- )}
-
-
-
Manual media files import.
-
- Add files to the cache/import folder. Make
- sure to follow the instructions in the Github{' '}
-
- Wiki
-
- .
-
-
- {processingImports &&
Processing import
}
- {!processingImports && (
-
{
- await updateTaskByName('manual_import');
- setProcessingImports(true);
- }}
- />
- )}
-
-
-
-
Embed thumbnails into media file.
-
Set extracted youtube thumbnail as cover art of the media file.
-
- {reEmbed &&
Processing thumbnails
}
- {!reEmbed && (
-
{
- await updateTaskByName('resync_thumbs');
- setReEmbed(true);
- }}
- />
- )}
-
-
-
-
ZIP file index backup
-
- Export your database to a zip file stored at{' '}
- cache/backup .
-
-
-
- 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.
-
-
-
- {backupStarted &&
Backing up archive
}
- {!backupStarted && (
-
{
- await queueBackup();
- setBackupStarted(true);
- }}
- />
- )}
-
-
-
-
Restore from backup
-
- Danger Zone : This will replace your existing index
- with the backup.
-
-
- Restore from available backup files from{' '}
- cache/backup .
-
- {!hasBackups &&
No backups found.
}
- {hasBackups && (
- <>
-
-
- Timestamp
- Source
- Filename
-
- {isRestoringBackup &&
Restoring from backup
}
- {!isRestoringBackup &&
- backups.map(backup => {
- return (
-
- {
- await restoreBackup(backup.filename);
- setIsRestoringBackup(true);
- }}
- />
- {backup.timestamp}
- {backup.reason}
- {backup.filename}
-
- );
- })}
- >
- )}
-
-
-
Rescan filesystem
-
- Danger Zone : This will delete the metadata of
- deleted videos from the filesystem.
-
-
- Rescan your media folder looking for missing videos and clean up index. More infos on
- the Github{' '}
-
- Wiki
-
- .
-
-
- {reScanningFileSystem &&
File system scan in progress
}
- {!reScanningFileSystem && (
-
{
- await updateTaskByName('rescan_filesystem');
- setReScanningFileSystem(true);
- }}
- />
- )}
-
-
-
- >
- );
-};
-
-export default SettingsActions;
+import { useEffect, useState } from 'react';
+import loadBackupList from '../api/loader/loadBackupList';
+import SettingsNavigation from '../components/SettingsNavigation';
+import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
+import updateTaskByName from '../api/actions/updateTaskByName';
+import queueBackup from '../api/actions/queueBackup';
+import restoreBackup from '../api/actions/restoreBackup';
+import Notifications from '../components/Notifications';
+import Button from '../components/Button';
+
+type Backup = {
+ filename: string;
+ file_path: string;
+ file_size: number;
+ timestamp: string;
+ reason: string;
+};
+
+type BackupListType = Backup[];
+
+const SettingsActions = () => {
+ const [deleteIgnored, setDeleteIgnored] = useState(false);
+ const [deletePending, setDeletePending] = useState(false);
+ const [processingImports, setProcessingImports] = useState(false);
+ const [reEmbed, setReEmbed] = useState(false);
+ const [backupStarted, setBackupStarted] = useState(false);
+ const [isRestoringBackup, setIsRestoringBackup] = useState(false);
+ const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
+
+ const [backupListResponse, setBackupListResponse] = useState();
+
+ const backups = backupListResponse;
+ const hasBackups = !!backups && backups?.length > 0;
+
+ useEffect(() => {
+ (async () => {
+ const backupListResponse = await loadBackupList();
+
+ setBackupListResponse(backupListResponse);
+ })();
+ }, []);
+
+ return (
+ <>
+ TA | Actions
+
+
+
{
+ setDeleteIgnored(false);
+ setDeletePending(false);
+ setProcessingImports(false);
+ setReEmbed(false);
+ setBackupStarted(false);
+ setIsRestoringBackup(false);
+ setReScanningFileSystem(false);
+ }}
+ />
+
+
+
Actions
+
+
+
Delete download queue
+
Delete your pending or previously ignored videos from your download queue.
+ {deleteIgnored &&
Deleting download queue: ignored
}
+ {!deleteIgnored && (
+
{
+ await deleteDownloadQueueByFilter('ignore');
+ setDeleteIgnored(true);
+ }}
+ />
+ )}{' '}
+ {deletePending && Deleting download queue: pending
}
+ {!deletePending && (
+ {
+ await deleteDownloadQueueByFilter('pending');
+ setDeletePending(true);
+ }}
+ />
+ )}
+
+
+
Manual media files import.
+
+ Add files to the cache/import folder. Make
+ sure to follow the instructions in the Github{' '}
+
+ Wiki
+
+ .
+
+
+ {processingImports &&
Processing import
}
+ {!processingImports && (
+
{
+ await updateTaskByName('manual_import');
+ setProcessingImports(true);
+ }}
+ />
+ )}
+
+
+
+
Embed thumbnails into media file.
+
Set extracted youtube thumbnail as cover art of the media file.
+
+ {reEmbed &&
Processing thumbnails
}
+ {!reEmbed && (
+
{
+ await updateTaskByName('resync_thumbs');
+ setReEmbed(true);
+ }}
+ />
+ )}
+
+
+
+
ZIP file index backup
+
+ Export your database to a zip file stored at{' '}
+ cache/backup .
+
+
+
+ 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.
+
+
+
+ {backupStarted &&
Backing up archive
}
+ {!backupStarted && (
+
{
+ await queueBackup();
+ setBackupStarted(true);
+ }}
+ />
+ )}
+
+
+
+
Restore from backup
+
+ Danger Zone : This will replace your existing index
+ with the backup.
+
+
+ Restore from available backup files from{' '}
+ cache/backup .
+
+ {!hasBackups &&
No backups found.
}
+ {hasBackups && (
+ <>
+
+
+ Timestamp
+ Source
+ Filename
+
+ {isRestoringBackup &&
Restoring from backup
}
+ {!isRestoringBackup &&
+ backups.map(backup => {
+ return (
+
+ {
+ await restoreBackup(backup.filename);
+ setIsRestoringBackup(true);
+ }}
+ />
+ {backup.timestamp}
+ {backup.reason}
+ {backup.filename}
+
+ );
+ })}
+ >
+ )}
+
+
+
Rescan filesystem
+
+ Danger Zone : This will delete the metadata of
+ deleted videos from the filesystem.
+
+
+ Rescan your media folder looking for missing videos and clean up index. More infos on
+ the Github{' '}
+
+ Wiki
+
+ .
+
+
+ {reScanningFileSystem &&
File system scan in progress
}
+ {!reScanningFileSystem && (
+
{
+ await updateTaskByName('rescan_filesystem');
+ setReScanningFileSystem(true);
+ }}
+ />
+ )}
+
+
+
+ >
+ );
+};
+
+export default SettingsActions;
diff --git a/frontend/src/pages/SettingsApplication.tsx b/frontend/src/pages/SettingsApplication.tsx
index 2732ef96..d1200171 100644
--- a/frontend/src/pages/SettingsApplication.tsx
+++ b/frontend/src/pages/SettingsApplication.tsx
@@ -1,965 +1,965 @@
-import { useEffect, useState } from 'react';
-import loadSnapshots from '../api/loader/loadSnapshots';
-import Notifications from '../components/Notifications';
-import PaginationDummy from '../components/PaginationDummy';
-import SettingsNavigation from '../components/SettingsNavigation';
-import restoreSnapshot from '../api/actions/restoreSnapshot';
-import queueSnapshot from '../api/actions/queueSnapshot';
-import updateCookie, { ValidatedCookieType } from '../api/actions/updateCookie';
-import deleteApiToken from '../api/actions/deleteApiToken';
-import Button from '../components/Button';
-import loadAppsettingsConfig, { AppSettingsConfigType } from '../api/loader/loadAppsettingsConfig';
-import updateAppsettingsConfig from '../api/actions/updateAppsettingsConfig';
-import loadApiToken from '../api/loader/loadApiToken';
-
-type SnapshotType = {
- id: string;
- state: string;
- es_version: string;
- start_date: string;
- end_date: string;
- end_stamp: number;
- duration_s: number;
-};
-
-type SnapshotListType = {
- next_exec: number;
- next_exec_str: string;
- expire_after: string;
- snapshots?: SnapshotType[];
-};
-
-type SettingsApplicationReponses = {
- snapshots?: SnapshotListType;
- appSettingsConfig?: AppSettingsConfigType;
- apiToken?: string;
-};
-
-const SettingsApplication = () => {
- const [response, setResponse] = useState();
- const [refresh, setRefresh] = useState(false);
-
- const snapshots = response?.snapshots;
- const appSettingsConfig = response?.appSettingsConfig;
- const apiToken = response?.apiToken;
-
- // Subscriptions
- const [videoPageSize, setVideoPageSize] = useState(0);
- const [livePageSize, setLivePageSize] = useState(0);
- const [shortPageSize, setShortPageSize] = useState(0);
- const [isAutostart, setIsAutostart] = useState(false);
-
- // Downloads
- const [currentDownloadSpeed, setCurrentDownloadSpeed] = useState(0);
- const [currentThrottledRate, setCurrentThrottledRate] = useState(0);
- const [currentScrapingSleep, setCurrentScrapingSleep] = useState(0);
- const [currentAutodelete, setCurrentAutodelete] = useState(0);
-
- // Download Format
- const [downloadsFormat, setDownloadsFormat] = useState('');
- const [downloadsFormatSort, setDownloadsFormatSort] = useState('');
- const [downloadsExtractorLang, setDownloadsExtractorLang] = useState('');
- const [embedMetadata, setEmbedMetadata] = useState(false);
- const [embedThumbnail, setEmbedThumbnail] = useState(false);
-
- // Subtitles
- const [subtitleLang, setSubtitleLang] = useState('');
- const [subtitleSource, setSubtitleSource] = useState('');
- const [indexSubtitles, setIndexSubtitles] = useState(false);
-
- // Comments
- const [commentsMax, setCommentsMax] = useState('');
- const [commentsSort, setCommentsSort] = useState('');
-
- // Cookie
- const [cookieImport, setCookieImport] = useState(false);
- const [validatingCookie, setValidatingCookie] = useState(false);
- const [cookieResponse, setCookieResponse] = useState();
-
- // Integrations
- const [showApiToken, setShowApiToken] = useState(false);
- const [downloadDislikes, setDownloadDislikes] = useState(false);
- const [enableSponsorBlock, setEnableSponsorBlock] = useState(false);
- const [resetTokenResponse, setResetTokenResponse] = useState({});
-
- // Snapshots
- const [enableSnapshots, setEnableSnapshots] = useState(false);
- const [isSnapshotQueued, setIsSnapshotQueued] = useState(false);
- const [restoringSnapshot, setRestoringSnapshot] = useState(false);
-
- const onSubmit = async () => {
- await updateAppsettingsConfig({
- application: {
- enable_snapshot: enableSnapshots,
- },
- downloads: {
- limit_speed: currentDownloadSpeed,
- sleep_interval: currentScrapingSleep,
- autodelete_days: currentAutodelete,
- format: downloadsFormat,
- format_sort: downloadsFormatSort,
- add_metadata: embedMetadata,
- add_thumbnail: embedThumbnail,
- subtitle: subtitleLang,
- subtitle_source: subtitleSource,
- subtitle_index: indexSubtitles,
- comment_max: commentsMax,
- comment_sort: commentsSort,
- cookie_import: cookieImport,
- throttledratelimit: currentThrottledRate,
- extractor_lang: downloadsExtractorLang,
- integrate_ryd: downloadDislikes,
- integrate_sponsorblock: enableSponsorBlock,
- },
- subscriptions: {
- auto_start: isAutostart,
- channel_size: videoPageSize,
- live_channel_size: livePageSize,
- shorts_channel_size: shortPageSize,
- },
- });
-
- setRefresh(true);
- };
-
- const fetchData = async () => {
- const snapshotResponse = await loadSnapshots();
- const appSettingsConfig = await loadAppsettingsConfig();
- const apiToken = await loadApiToken();
-
- // Subscriptions
- setVideoPageSize(appSettingsConfig?.subscriptions.channel_size);
- setLivePageSize(appSettingsConfig?.subscriptions.live_channel_size);
- setShortPageSize(appSettingsConfig?.subscriptions.shorts_channel_size);
- setIsAutostart(appSettingsConfig?.subscriptions.auto_start);
-
- // Downloads
- setCurrentDownloadSpeed(appSettingsConfig?.downloads.limit_speed || 0);
- setCurrentThrottledRate(appSettingsConfig?.downloads.throttledratelimit || 0);
- setCurrentScrapingSleep(appSettingsConfig?.downloads.sleep_interval);
- setCurrentAutodelete(appSettingsConfig?.downloads.autodelete_days);
-
- // Download Format
- setDownloadsFormat(appSettingsConfig?.downloads.format.toString());
- setDownloadsFormatSort(appSettingsConfig?.downloads.format_sort.toString());
- setDownloadsExtractorLang(appSettingsConfig?.downloads.extractor_lang.toString());
- setEmbedMetadata(appSettingsConfig?.downloads.add_metadata);
- setEmbedThumbnail(appSettingsConfig?.downloads.add_thumbnail);
-
- // Subtitles
- setSubtitleLang(appSettingsConfig?.downloads.subtitle.toString());
- setSubtitleSource(appSettingsConfig?.downloads.subtitle_source.toString());
- setIndexSubtitles(appSettingsConfig?.downloads.subtitle_index);
-
- // Comments
- setCommentsMax(appSettingsConfig?.downloads.comment_max.toString());
- setCommentsSort(appSettingsConfig?.downloads.comment_sort);
-
- // Cookie
- setCookieImport(appSettingsConfig?.downloads.cookie_import);
-
- // Integrations
- setDownloadDislikes(appSettingsConfig?.downloads.integrate_ryd);
- setEnableSponsorBlock(appSettingsConfig?.downloads.integrate_sponsorblock);
-
- // Snapshots
- setEnableSnapshots(appSettingsConfig?.application.enable_snapshot);
-
- setResponse({
- snapshots: snapshotResponse,
- appSettingsConfig,
- apiToken: apiToken.token,
- });
- };
-
- useEffect(() => {
- fetchData();
- }, []);
-
- useEffect(() => {
- if (refresh) {
- fetchData();
- setRefresh(false);
- }
- }, [refresh]);
-
- return (
- <>
- TA | Application Settings
-
-
-
-
-
-
Application Configurations
-
-
-
-
-
- >
- );
-};
-
-export default SettingsApplication;
+import { useEffect, useState } from 'react';
+import loadSnapshots from '../api/loader/loadSnapshots';
+import Notifications from '../components/Notifications';
+import PaginationDummy from '../components/PaginationDummy';
+import SettingsNavigation from '../components/SettingsNavigation';
+import restoreSnapshot from '../api/actions/restoreSnapshot';
+import queueSnapshot from '../api/actions/queueSnapshot';
+import updateCookie, { ValidatedCookieType } from '../api/actions/updateCookie';
+import deleteApiToken from '../api/actions/deleteApiToken';
+import Button from '../components/Button';
+import loadAppsettingsConfig, { AppSettingsConfigType } from '../api/loader/loadAppsettingsConfig';
+import updateAppsettingsConfig from '../api/actions/updateAppsettingsConfig';
+import loadApiToken from '../api/loader/loadApiToken';
+
+type SnapshotType = {
+ id: string;
+ state: string;
+ es_version: string;
+ start_date: string;
+ end_date: string;
+ end_stamp: number;
+ duration_s: number;
+};
+
+type SnapshotListType = {
+ next_exec: number;
+ next_exec_str: string;
+ expire_after: string;
+ snapshots?: SnapshotType[];
+};
+
+type SettingsApplicationReponses = {
+ snapshots?: SnapshotListType;
+ appSettingsConfig?: AppSettingsConfigType;
+ apiToken?: string;
+};
+
+const SettingsApplication = () => {
+ const [response, setResponse] = useState();
+ const [refresh, setRefresh] = useState(false);
+
+ const snapshots = response?.snapshots;
+ const appSettingsConfig = response?.appSettingsConfig;
+ const apiToken = response?.apiToken;
+
+ // Subscriptions
+ const [videoPageSize, setVideoPageSize] = useState(0);
+ const [livePageSize, setLivePageSize] = useState(0);
+ const [shortPageSize, setShortPageSize] = useState(0);
+ const [isAutostart, setIsAutostart] = useState(false);
+
+ // Downloads
+ const [currentDownloadSpeed, setCurrentDownloadSpeed] = useState(0);
+ const [currentThrottledRate, setCurrentThrottledRate] = useState(0);
+ const [currentScrapingSleep, setCurrentScrapingSleep] = useState(0);
+ const [currentAutodelete, setCurrentAutodelete] = useState(0);
+
+ // Download Format
+ const [downloadsFormat, setDownloadsFormat] = useState('');
+ const [downloadsFormatSort, setDownloadsFormatSort] = useState('');
+ const [downloadsExtractorLang, setDownloadsExtractorLang] = useState('');
+ const [embedMetadata, setEmbedMetadata] = useState(false);
+ const [embedThumbnail, setEmbedThumbnail] = useState(false);
+
+ // Subtitles
+ const [subtitleLang, setSubtitleLang] = useState('');
+ const [subtitleSource, setSubtitleSource] = useState('');
+ const [indexSubtitles, setIndexSubtitles] = useState(false);
+
+ // Comments
+ const [commentsMax, setCommentsMax] = useState('');
+ const [commentsSort, setCommentsSort] = useState('');
+
+ // Cookie
+ const [cookieImport, setCookieImport] = useState(false);
+ const [validatingCookie, setValidatingCookie] = useState(false);
+ const [cookieResponse, setCookieResponse] = useState();
+
+ // Integrations
+ const [showApiToken, setShowApiToken] = useState(false);
+ const [downloadDislikes, setDownloadDislikes] = useState(false);
+ const [enableSponsorBlock, setEnableSponsorBlock] = useState(false);
+ const [resetTokenResponse, setResetTokenResponse] = useState({});
+
+ // Snapshots
+ const [enableSnapshots, setEnableSnapshots] = useState(false);
+ const [isSnapshotQueued, setIsSnapshotQueued] = useState(false);
+ const [restoringSnapshot, setRestoringSnapshot] = useState(false);
+
+ const onSubmit = async () => {
+ await updateAppsettingsConfig({
+ application: {
+ enable_snapshot: enableSnapshots,
+ },
+ downloads: {
+ limit_speed: currentDownloadSpeed,
+ sleep_interval: currentScrapingSleep,
+ autodelete_days: currentAutodelete,
+ format: downloadsFormat,
+ format_sort: downloadsFormatSort,
+ add_metadata: embedMetadata,
+ add_thumbnail: embedThumbnail,
+ subtitle: subtitleLang,
+ subtitle_source: subtitleSource,
+ subtitle_index: indexSubtitles,
+ comment_max: commentsMax,
+ comment_sort: commentsSort,
+ cookie_import: cookieImport,
+ throttledratelimit: currentThrottledRate,
+ extractor_lang: downloadsExtractorLang,
+ integrate_ryd: downloadDislikes,
+ integrate_sponsorblock: enableSponsorBlock,
+ },
+ subscriptions: {
+ auto_start: isAutostart,
+ channel_size: videoPageSize,
+ live_channel_size: livePageSize,
+ shorts_channel_size: shortPageSize,
+ },
+ });
+
+ setRefresh(true);
+ };
+
+ const fetchData = async () => {
+ const snapshotResponse = await loadSnapshots();
+ const appSettingsConfig = await loadAppsettingsConfig();
+ const apiToken = await loadApiToken();
+
+ // Subscriptions
+ setVideoPageSize(appSettingsConfig?.subscriptions.channel_size);
+ setLivePageSize(appSettingsConfig?.subscriptions.live_channel_size);
+ setShortPageSize(appSettingsConfig?.subscriptions.shorts_channel_size);
+ setIsAutostart(appSettingsConfig?.subscriptions.auto_start);
+
+ // Downloads
+ setCurrentDownloadSpeed(appSettingsConfig?.downloads.limit_speed || 0);
+ setCurrentThrottledRate(appSettingsConfig?.downloads.throttledratelimit || 0);
+ setCurrentScrapingSleep(appSettingsConfig?.downloads.sleep_interval);
+ setCurrentAutodelete(appSettingsConfig?.downloads.autodelete_days);
+
+ // Download Format
+ setDownloadsFormat(appSettingsConfig?.downloads.format.toString());
+ setDownloadsFormatSort(appSettingsConfig?.downloads.format_sort.toString());
+ setDownloadsExtractorLang(appSettingsConfig?.downloads.extractor_lang.toString());
+ setEmbedMetadata(appSettingsConfig?.downloads.add_metadata);
+ setEmbedThumbnail(appSettingsConfig?.downloads.add_thumbnail);
+
+ // Subtitles
+ setSubtitleLang(appSettingsConfig?.downloads.subtitle.toString());
+ setSubtitleSource(appSettingsConfig?.downloads.subtitle_source.toString());
+ setIndexSubtitles(appSettingsConfig?.downloads.subtitle_index);
+
+ // Comments
+ setCommentsMax(appSettingsConfig?.downloads.comment_max.toString());
+ setCommentsSort(appSettingsConfig?.downloads.comment_sort);
+
+ // Cookie
+ setCookieImport(appSettingsConfig?.downloads.cookie_import);
+
+ // Integrations
+ setDownloadDislikes(appSettingsConfig?.downloads.integrate_ryd);
+ setEnableSponsorBlock(appSettingsConfig?.downloads.integrate_sponsorblock);
+
+ // Snapshots
+ setEnableSnapshots(appSettingsConfig?.application.enable_snapshot);
+
+ setResponse({
+ snapshots: snapshotResponse,
+ appSettingsConfig,
+ apiToken: apiToken.token,
+ });
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ if (refresh) {
+ fetchData();
+ setRefresh(false);
+ }
+ }, [refresh]);
+
+ return (
+ <>
+ TA | Application Settings
+
+
+
+
+
+
Application Configurations
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsApplication;
diff --git a/frontend/src/pages/SettingsDashboard.tsx b/frontend/src/pages/SettingsDashboard.tsx
index 1f58499c..23a81d5b 100644
--- a/frontend/src/pages/SettingsDashboard.tsx
+++ b/frontend/src/pages/SettingsDashboard.tsx
@@ -1,260 +1,260 @@
-import { useEffect, useState } from 'react';
-import SettingsNavigation from '../components/SettingsNavigation';
-import loadStatsVideo from '../api/loader/loadStatsVideo';
-import loadStatsChannel from '../api/loader/loadStatsChannel';
-import loadStatsPlaylist from '../api/loader/loadStatsPlaylist';
-import loadStatsDownload from '../api/loader/loadStatsDownload';
-import loadStatsWatchProgress from '../api/loader/loadStatsWatchProgress';
-import loadStatsDownloadHistory from '../api/loader/loadStatsDownloadHistory';
-import loadStatsBiggestChannels from '../api/loader/loadStatsBiggestChannels';
-import OverviewStats from '../components/OverviewStats';
-import VideoTypeStats from '../components/VideoTypeStats';
-import ApplicationStats from '../components/ApplicationStats';
-import WatchProgressStats from '../components/WatchProgressStats';
-import DownloadHistoryStats from '../components/DownloadHistoryStats';
-import BiggestChannelsStats from '../components/BiggestChannelsStats';
-import Notifications from '../components/Notifications';
-import PaginationDummy from '../components/PaginationDummy';
-
-export type VideoStatsType = {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- type_videos: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- type_shorts: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- active_true: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- active_false: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
- type_streams: {
- doc_count: number;
- media_size: number;
- duration: number;
- duration_str: string;
- };
-};
-
-export type ChannelStatsType = {
- doc_count: number;
- active_true: number;
- subscribed_true: number;
-};
-
-export type PlaylistStatsType = {
- doc_count: number;
- active_false: number;
- active_true: number;
- subscribed_true: number;
-};
-
-export type DownloadStatsType = {
- pending: number;
- pending_videos: number;
- pending_shorts: number;
- pending_streams: number;
-};
-
-export type WatchProgressStatsType = {
- total: {
- duration: number;
- duration_str: string;
- items: number;
- };
- unwatched: {
- duration: number;
- duration_str: string;
- progress: number;
- items: number;
- };
- watched: {
- duration: number;
- duration_str: string;
- progress: number;
- items: number;
- };
-};
-
-type DownloadHistoryType = {
- date: string;
- count: number;
- media_size: number;
-};
-
-export type DownloadHistoryStatsType = DownloadHistoryType[];
-
-type BiggestChannelsType = {
- id: string;
- name: string;
- doc_count: number;
- duration: number;
- duration_str: string;
- media_size: number;
-};
-
-export type BiggestChannelsStatsType = BiggestChannelsType[];
-
-type DashboardStatsReponses = {
- videoStats?: VideoStatsType;
- channelStats?: ChannelStatsType;
- playlistStats?: PlaylistStatsType;
- downloadStats?: DownloadStatsType;
- watchProgressStats?: WatchProgressStatsType;
- downloadHistoryStats?: DownloadHistoryStatsType;
- biggestChannelsStatsByCount?: BiggestChannelsStatsType;
- biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
- biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
-};
-
-const SettingsDashboard = () => {
- const [useSi, setUseSi] = useState(false);
-
- const [response, setResponse] = useState({
- videoStats: undefined,
- });
-
- const videoStats = response?.videoStats;
- const channelStats = response?.channelStats;
- const playlistStats = response?.playlistStats;
- const downloadStats = response?.downloadStats;
- const watchProgressStats = response?.watchProgressStats;
- const downloadHistoryStats = response?.downloadHistoryStats;
- const biggestChannelsStatsByCount = response?.biggestChannelsStatsByCount;
- const biggestChannelsStatsByDuration = response?.biggestChannelsStatsByDuration;
- const biggestChannelsStatsByMediaSize = response?.biggestChannelsStatsByMediaSize;
-
- useEffect(() => {
- (async () => {
- const all = await Promise.all([
- await loadStatsVideo(),
- await loadStatsChannel(),
- await loadStatsPlaylist(),
- await loadStatsDownload(),
- await loadStatsWatchProgress(),
- await loadStatsDownloadHistory(),
- await loadStatsBiggestChannels('doc_count'),
- await loadStatsBiggestChannels('duration'),
- await loadStatsBiggestChannels('media_size'),
- ]);
-
- const [
- videoStats,
- channelStats,
- playlistStats,
- downloadStats,
- watchProgressStats,
- downloadHistoryStats,
- biggestChannelsStatsByCount,
- biggestChannelsStatsByDuration,
- biggestChannelsStatsByMediaSize,
- ] = all;
-
- setResponse({
- videoStats,
- channelStats,
- playlistStats,
- downloadStats,
- watchProgressStats,
- downloadHistoryStats,
- biggestChannelsStatsByCount,
- biggestChannelsStatsByDuration,
- biggestChannelsStatsByMediaSize,
- });
- })();
- }, []);
-
- return (
- <>
- TA | Settings Dashboard
-
-
-
-
-
Your Archive
-
-
- File Sizes in:
- {
- const value = event.target.value;
- console.log(value);
- setUseSi(value === 'true');
- }}
- >
- SI units
- Binary units
-
-
-
-
-
-
-
-
-
Download History
-
-
-
-
-
-
Biggest Channels
-
-
-
-
-
-
-
- >
- );
-};
-
-export default SettingsDashboard;
+import { useEffect, useState } from 'react';
+import SettingsNavigation from '../components/SettingsNavigation';
+import loadStatsVideo from '../api/loader/loadStatsVideo';
+import loadStatsChannel from '../api/loader/loadStatsChannel';
+import loadStatsPlaylist from '../api/loader/loadStatsPlaylist';
+import loadStatsDownload from '../api/loader/loadStatsDownload';
+import loadStatsWatchProgress from '../api/loader/loadStatsWatchProgress';
+import loadStatsDownloadHistory from '../api/loader/loadStatsDownloadHistory';
+import loadStatsBiggestChannels from '../api/loader/loadStatsBiggestChannels';
+import OverviewStats from '../components/OverviewStats';
+import VideoTypeStats from '../components/VideoTypeStats';
+import ApplicationStats from '../components/ApplicationStats';
+import WatchProgressStats from '../components/WatchProgressStats';
+import DownloadHistoryStats from '../components/DownloadHistoryStats';
+import BiggestChannelsStats from '../components/BiggestChannelsStats';
+import Notifications from '../components/Notifications';
+import PaginationDummy from '../components/PaginationDummy';
+
+export type VideoStatsType = {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ type_videos: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ type_shorts: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ active_true: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ active_false: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+ type_streams: {
+ doc_count: number;
+ media_size: number;
+ duration: number;
+ duration_str: string;
+ };
+};
+
+export type ChannelStatsType = {
+ doc_count: number;
+ active_true: number;
+ subscribed_true: number;
+};
+
+export type PlaylistStatsType = {
+ doc_count: number;
+ active_false: number;
+ active_true: number;
+ subscribed_true: number;
+};
+
+export type DownloadStatsType = {
+ pending: number;
+ pending_videos: number;
+ pending_shorts: number;
+ pending_streams: number;
+};
+
+export type WatchProgressStatsType = {
+ total: {
+ duration: number;
+ duration_str: string;
+ items: number;
+ };
+ unwatched: {
+ duration: number;
+ duration_str: string;
+ progress: number;
+ items: number;
+ };
+ watched: {
+ duration: number;
+ duration_str: string;
+ progress: number;
+ items: number;
+ };
+};
+
+type DownloadHistoryType = {
+ date: string;
+ count: number;
+ media_size: number;
+};
+
+export type DownloadHistoryStatsType = DownloadHistoryType[];
+
+type BiggestChannelsType = {
+ id: string;
+ name: string;
+ doc_count: number;
+ duration: number;
+ duration_str: string;
+ media_size: number;
+};
+
+export type BiggestChannelsStatsType = BiggestChannelsType[];
+
+type DashboardStatsReponses = {
+ videoStats?: VideoStatsType;
+ channelStats?: ChannelStatsType;
+ playlistStats?: PlaylistStatsType;
+ downloadStats?: DownloadStatsType;
+ watchProgressStats?: WatchProgressStatsType;
+ downloadHistoryStats?: DownloadHistoryStatsType;
+ biggestChannelsStatsByCount?: BiggestChannelsStatsType;
+ biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
+ biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
+};
+
+const SettingsDashboard = () => {
+ const [useSi, setUseSi] = useState(false);
+
+ const [response, setResponse] = useState({
+ videoStats: undefined,
+ });
+
+ const videoStats = response?.videoStats;
+ const channelStats = response?.channelStats;
+ const playlistStats = response?.playlistStats;
+ const downloadStats = response?.downloadStats;
+ const watchProgressStats = response?.watchProgressStats;
+ const downloadHistoryStats = response?.downloadHistoryStats;
+ const biggestChannelsStatsByCount = response?.biggestChannelsStatsByCount;
+ const biggestChannelsStatsByDuration = response?.biggestChannelsStatsByDuration;
+ const biggestChannelsStatsByMediaSize = response?.biggestChannelsStatsByMediaSize;
+
+ useEffect(() => {
+ (async () => {
+ const all = await Promise.all([
+ await loadStatsVideo(),
+ await loadStatsChannel(),
+ await loadStatsPlaylist(),
+ await loadStatsDownload(),
+ await loadStatsWatchProgress(),
+ await loadStatsDownloadHistory(),
+ await loadStatsBiggestChannels('doc_count'),
+ await loadStatsBiggestChannels('duration'),
+ await loadStatsBiggestChannels('media_size'),
+ ]);
+
+ const [
+ videoStats,
+ channelStats,
+ playlistStats,
+ downloadStats,
+ watchProgressStats,
+ downloadHistoryStats,
+ biggestChannelsStatsByCount,
+ biggestChannelsStatsByDuration,
+ biggestChannelsStatsByMediaSize,
+ ] = all;
+
+ setResponse({
+ videoStats,
+ channelStats,
+ playlistStats,
+ downloadStats,
+ watchProgressStats,
+ downloadHistoryStats,
+ biggestChannelsStatsByCount,
+ biggestChannelsStatsByDuration,
+ biggestChannelsStatsByMediaSize,
+ });
+ })();
+ }, []);
+
+ return (
+ <>
+ TA | Settings Dashboard
+
+
+
+
+
Your Archive
+
+
+ File Sizes in:
+ {
+ const value = event.target.value;
+ console.log(value);
+ setUseSi(value === 'true');
+ }}
+ >
+ SI units
+ Binary units
+
+
+
+
+
+
+
+
+
Download History
+
+
+
+
+
+
Biggest Channels
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsDashboard;
diff --git a/frontend/src/pages/SettingsScheduling.tsx b/frontend/src/pages/SettingsScheduling.tsx
index 99a8d230..12749d1f 100644
--- a/frontend/src/pages/SettingsScheduling.tsx
+++ b/frontend/src/pages/SettingsScheduling.tsx
@@ -1,495 +1,495 @@
-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([]);
- const [appriseNotification, setAppriseNotification] = useState();
-
- const [updateSubscribed, setUpdateSubscribed] = useState();
- const [downloadPending, setDownloadPending] = useState();
- const [checkReindex, setCheckReindex] = useState();
- const [checkReindexDays, setCheckReindexDays] = useState();
- const [thumbnailCheck, setThumbnailCheck] = useState();
- const [zipBackup, setZipBackup] = useState();
- const [zipBackupDays, setZipBackupDays] = useState();
- const [notificationUrl, setNotificationUrl] = useState();
- const [notificationTask, setNotificationTask] = useState('');
-
- 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 (
- <>
- TA | Scheduling Settings
-
-
-
-
-
-
Scheduler Setup
-
-
- Schedule settings expect a cron like format, where the first value is minute, second
- is hour and third is day of the week.
-
-
Examples:
-
-
- 0 15 * : Run task every day at 15:00 in the
- afternoon.
-
-
- 30 8 */2 : Run task every second day of the
- week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
-
-
- auto : Sensible default.
-
-
-
Note:
-
-
- 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.
-
-
-
-
-
-
-
Rescan Subscriptions
-
-
- Become a sponsor and join{' '}
-
- members.tubearchivist.com
- {' '}
- to get access to real time notifications for
- new videos uploaded by your favorite channels.
-
-
- Current rescan schedule:{' '}
-
- {!updateSubscribedSchedule && 'False'}
- {updateSubscribedSchedule && (
- <>
- {updateSubscribedSchedule?.schedule}{' '}
- {
- await deleteTaskSchedule('update_subscribed');
-
- setRefresh(true);
- }}
- className="danger-button"
- />
- >
- )}
-
-
-
Periodically rescan your subscriptions:
-
-
{
- setUpdateSubscribed(e.currentTarget.value);
- }}
- />
-
{
- await createTaskSchedule('update_subscribed', {
- schedule: updateSubscribed,
- });
-
- setUpdateSubscribed('');
-
- setRefresh(true);
- }}
- />
-
-
-
-
Start Download
-
-
- Current Download schedule:{' '}
-
- {!download_pending && 'False'}
- {downloadPendingSchedule && (
- <>
- {downloadPendingSchedule?.schedule}{' '}
- {
- await deleteTaskSchedule('download_pending');
-
- setRefresh(true);
- }}
- />
- >
- )}
-
-
-
Automatic video download schedule:
-
-
{
- setDownloadPending(e.currentTarget.value);
- }}
- />
-
{
- await createTaskSchedule('download_pending', {
- schedule: downloadPending,
- });
-
- setDownloadPending('');
-
- setRefresh(true);
- }}
- />
-
-
-
-
-
Refresh Metadata
-
-
- Current Metadata refresh schedule:{' '}
-
- {!checkReindexSchedule && 'False'}
- {checkReindexSchedule && (
- <>
- {checkReindexSchedule?.schedule}{' '}
- {
- await deleteTaskSchedule('check_reindex');
-
- setRefresh(true);
- }}
- />
- >
- )}
-
-
-
Daily schedule to refresh metadata from YouTube:
-
-
{
- setCheckReindex(e.currentTarget.value);
- }}
- />
-
{
- await createTaskSchedule('check_reindex', {
- schedule: checkReindex,
- });
-
- setCheckReindex('');
-
- setRefresh(true);
- }}
- />
-
-
-
- Current refresh for metadata older than x days:{' '}
- {checkReindexSchedule?.config?.days}
-
-
Refresh older than x days, recommended 90:
-
-
{
- setCheckReindexDays(Number(e.currentTarget.value));
- }}
- />
-
{
- await createTaskSchedule('check_reindex', {
- config: {
- days: checkReindexDays,
- },
- });
-
- setCheckReindexDays(undefined);
-
- setRefresh(true);
- }}
- />
-
-
-
-
-
Thumbnail Check
-
-
- Current thumbnail check schedule:{' '}
-
- {!thumbnailCheckSchedule && 'False'}
- {thumbnailCheckSchedule && (
- <>
- {thumbnailCheckSchedule?.schedule}{' '}
- {
- await deleteTaskSchedule('thumbnail_check');
-
- setRefresh(true);
- }}
- />
- >
- )}
-
-
-
Periodically check and cleanup thumbnails:
-
-
{
- setThumbnailCheck(e.currentTarget.value);
- }}
- />
-
{
- await createTaskSchedule('thumbnail_check', {
- schedule: thumbnailCheck,
- });
-
- setThumbnailCheck('');
-
- setRefresh(true);
- }}
- />
-
-
-
-
ZIP file index backup
-
-
-
- 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.
-
-
-
- Current index backup schedule:{' '}
-
- {!runBackup && 'False'}
- {runBackup && (
- <>
- {runBackup.schedule}{' '}
- {
- await deleteTaskSchedule('run_backup');
-
- setRefresh(true);
- }}
- />
- >
- )}
-
-
-
Automatically backup metadata to a zip file:
-
-
{
- setZipBackup(e.currentTarget.value);
- }}
- />
-
{
- await createTaskSchedule('run_backup', {
- schedule: zipBackup,
- });
-
- setZipBackup('');
-
- setRefresh(true);
- }}
- />
-
-
-
- Current backup files to keep:{' '}
- {runBackup?.config?.rotate}
-
-
Max auto backups to keep:
-
-
{
- setZipBackupDays(Number(e.currentTarget.value));
- }}
- />
-
{
- await createTaskSchedule('run_backup', {
- config: {
- rotate: zipBackupDays,
- },
- });
-
- setZipBackupDays(undefined);
-
- setRefresh(true);
- }}
- />
-
-
-
-
Add Notification URL
-
- {!appriseNotification &&
No notifications stored
}
- {appriseNotification && (
- <>
-
- {Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
- return (
- <>
-
{title}
- {urls.map((url: string) => {
- return (
-
- {url}
- {
- await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
-
- setRefresh(true);
- }}
- />
-
- );
- })}
- >
- );
- })}
-
- >
- )}
-
-
-
-
- Send notification on completed tasks with the help of the{' '}
-
- Apprise
- {' '}
- library.
-
-
-
{
- setNotificationTask(e.currentTarget.value);
- }}
- >
- -- select task --
- Rescan your Subscriptions
- Add to download queue
- Downloading
- Reindex Documents
-
-
-
{
- setNotificationUrl(e.currentTarget.value);
- }}
- />
-
{
- await createAppriseNotificationUrl(
- notificationTask as AppriseTaskNameType,
- notificationUrl || '',
- );
-
- setRefresh(true);
- }}
- />
-
-
-
-
-
- >
- );
-};
-
-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([]);
+ const [appriseNotification, setAppriseNotification] = useState();
+
+ const [updateSubscribed, setUpdateSubscribed] = useState();
+ const [downloadPending, setDownloadPending] = useState();
+ const [checkReindex, setCheckReindex] = useState();
+ const [checkReindexDays, setCheckReindexDays] = useState();
+ const [thumbnailCheck, setThumbnailCheck] = useState();
+ const [zipBackup, setZipBackup] = useState();
+ const [zipBackupDays, setZipBackupDays] = useState();
+ const [notificationUrl, setNotificationUrl] = useState();
+ const [notificationTask, setNotificationTask] = useState('');
+
+ 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 (
+ <>
+ TA | Scheduling Settings
+
+
+
+
+
+
Scheduler Setup
+
+
+ Schedule settings expect a cron like format, where the first value is minute, second
+ is hour and third is day of the week.
+
+
Examples:
+
+
+ 0 15 * : Run task every day at 15:00 in the
+ afternoon.
+
+
+ 30 8 */2 : Run task every second day of the
+ week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
+
+
+ auto : Sensible default.
+
+
+
Note:
+
+
+ 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.
+
+
+
+
+
+
+
Rescan Subscriptions
+
+
+ Become a sponsor and join{' '}
+
+ members.tubearchivist.com
+ {' '}
+ to get access to real time notifications for
+ new videos uploaded by your favorite channels.
+
+
+ Current rescan schedule:{' '}
+
+ {!updateSubscribedSchedule && 'False'}
+ {updateSubscribedSchedule && (
+ <>
+ {updateSubscribedSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('update_subscribed');
+
+ setRefresh(true);
+ }}
+ className="danger-button"
+ />
+ >
+ )}
+
+
+
Periodically rescan your subscriptions:
+
+
{
+ setUpdateSubscribed(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('update_subscribed', {
+ schedule: updateSubscribed,
+ });
+
+ setUpdateSubscribed('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
Start Download
+
+
+ Current Download schedule:{' '}
+
+ {!download_pending && 'False'}
+ {downloadPendingSchedule && (
+ <>
+ {downloadPendingSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('download_pending');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Automatic video download schedule:
+
+
{
+ setDownloadPending(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('download_pending', {
+ schedule: downloadPending,
+ });
+
+ setDownloadPending('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
+
Refresh Metadata
+
+
+ Current Metadata refresh schedule:{' '}
+
+ {!checkReindexSchedule && 'False'}
+ {checkReindexSchedule && (
+ <>
+ {checkReindexSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('check_reindex');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Daily schedule to refresh metadata from YouTube:
+
+
{
+ setCheckReindex(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('check_reindex', {
+ schedule: checkReindex,
+ });
+
+ setCheckReindex('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+ Current refresh for metadata older than x days:{' '}
+ {checkReindexSchedule?.config?.days}
+
+
Refresh older than x days, recommended 90:
+
+
{
+ setCheckReindexDays(Number(e.currentTarget.value));
+ }}
+ />
+
{
+ await createTaskSchedule('check_reindex', {
+ config: {
+ days: checkReindexDays,
+ },
+ });
+
+ setCheckReindexDays(undefined);
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
+
Thumbnail Check
+
+
+ Current thumbnail check schedule:{' '}
+
+ {!thumbnailCheckSchedule && 'False'}
+ {thumbnailCheckSchedule && (
+ <>
+ {thumbnailCheckSchedule?.schedule}{' '}
+ {
+ await deleteTaskSchedule('thumbnail_check');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Periodically check and cleanup thumbnails:
+
+
{
+ setThumbnailCheck(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('thumbnail_check', {
+ schedule: thumbnailCheck,
+ });
+
+ setThumbnailCheck('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
ZIP file index backup
+
+
+
+ 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.
+
+
+
+ Current index backup schedule:{' '}
+
+ {!runBackup && 'False'}
+ {runBackup && (
+ <>
+ {runBackup.schedule}{' '}
+ {
+ await deleteTaskSchedule('run_backup');
+
+ setRefresh(true);
+ }}
+ />
+ >
+ )}
+
+
+
Automatically backup metadata to a zip file:
+
+
{
+ setZipBackup(e.currentTarget.value);
+ }}
+ />
+
{
+ await createTaskSchedule('run_backup', {
+ schedule: zipBackup,
+ });
+
+ setZipBackup('');
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+ Current backup files to keep:{' '}
+ {runBackup?.config?.rotate}
+
+
Max auto backups to keep:
+
+
{
+ setZipBackupDays(Number(e.currentTarget.value));
+ }}
+ />
+
{
+ await createTaskSchedule('run_backup', {
+ config: {
+ rotate: zipBackupDays,
+ },
+ });
+
+ setZipBackupDays(undefined);
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
Add Notification URL
+
+ {!appriseNotification &&
No notifications stored
}
+ {appriseNotification && (
+ <>
+
+ {Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
+ return (
+ <>
+
{title}
+ {urls.map((url: string) => {
+ return (
+
+ {url}
+ {
+ await deleteAppriseNotificationUrl(key as AppriseTaskNameType);
+
+ setRefresh(true);
+ }}
+ />
+
+ );
+ })}
+ >
+ );
+ })}
+
+ >
+ )}
+
+
+
+
+ Send notification on completed tasks with the help of the{' '}
+
+ Apprise
+ {' '}
+ library.
+
+
+
{
+ setNotificationTask(e.currentTarget.value);
+ }}
+ >
+ -- select task --
+ Rescan your Subscriptions
+ Add to download queue
+ Downloading
+ Reindex Documents
+
+
+
{
+ setNotificationUrl(e.currentTarget.value);
+ }}
+ />
+
{
+ await createAppriseNotificationUrl(
+ notificationTask as AppriseTaskNameType,
+ notificationUrl || '',
+ );
+
+ setRefresh(true);
+ }}
+ />
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsScheduling;
diff --git a/frontend/src/pages/SettingsUser.tsx b/frontend/src/pages/SettingsUser.tsx
index 463df003..deb4a8b4 100644
--- a/frontend/src/pages/SettingsUser.tsx
+++ b/frontend/src/pages/SettingsUser.tsx
@@ -1,126 +1,126 @@
-import { useNavigate } from 'react-router-dom';
-import { ColourVariants } from '../api/actions/updateUserConfig';
-import { ColourConstant } from '../configuration/colours/getColours';
-import SettingsNavigation from '../components/SettingsNavigation';
-import Notifications from '../components/Notifications';
-import Button from '../components/Button';
-import loadIsAdmin from '../functions/getIsAdmin';
-import { useUserConfigStore } from '../stores/UserConfigStore';
-import { useEffect, useState } from 'react';
-
-const SettingsUser = () => {
- const { userConfig, setPartialConfig } = useUserConfigStore();
- const isAdmin = loadIsAdmin();
- const navigate = useNavigate();
-
- const [styleSheet, setStyleSheet] = useState(userConfig.config.stylesheet);
- const [styleSheetRefresh, setStyleSheetRefresh] = useState(false);
- const [pageSize, setPageSize] = useState(userConfig.config.page_size);
-
- useEffect(() => {
- (async () => {
- setStyleSheet(userConfig.config.stylesheet);
- setPageSize(userConfig.config.page_size);
- })();
- }, [userConfig.config.page_size, userConfig.config.stylesheet]);
-
- const handleStyleSheetChange = async (selectedStyleSheet: ColourVariants) => {
- setPartialConfig({stylesheet: selectedStyleSheet});
- setStyleSheet(selectedStyleSheet);
- setStyleSheetRefresh(true);
- }
-
- const handlePageSizeChange = async () => {
- setPartialConfig({page_size: pageSize});
- }
-
- const handlePageRefresh = () => {
- navigate(0);
- setStyleSheetRefresh(false);
- }
-
- return (
- <>
- TA | User Settings
-
-
-
-
-
-
User Configurations
-
-
-
-
Customize user Interface
-
-
-
Switch your color scheme
-
-
- {
- handleStyleSheetChange(event.target.value as ColourVariants);
- }}
- >
- {Object.entries(ColourConstant).map(([key, value]) => {
- return (
-
- {key}
-
- );
- })}
-
- {styleSheetRefresh && (
- Refresh
- )}
-
-
-
-
-
Archive view page size
-
-
-
{
- setPageSize(Number(event.target.value));
- }}
- />
-
- {userConfig.config.page_size !== pageSize && (
- <>
- Update
- setPageSize(userConfig.config.page_size)}>Cancel
- >
- )}
-
-
-
-
-
- {isAdmin && (
- <>
-
-
User Management
-
- Access the admin interface for basic user management functionality like adding and
- deleting users, changing passwords and more.
-
-
-
-
-
- >
- )}
-
- >
- );
-};
-
-export default SettingsUser;
+import { useNavigate } from 'react-router-dom';
+import { ColourVariants } from '../api/actions/updateUserConfig';
+import { ColourConstant } from '../configuration/colours/getColours';
+import SettingsNavigation from '../components/SettingsNavigation';
+import Notifications from '../components/Notifications';
+import Button from '../components/Button';
+import loadIsAdmin from '../functions/getIsAdmin';
+import { useUserConfigStore } from '../stores/UserConfigStore';
+import { useEffect, useState } from 'react';
+
+const SettingsUser = () => {
+ const { userConfig, setPartialConfig } = useUserConfigStore();
+ const isAdmin = loadIsAdmin();
+ const navigate = useNavigate();
+
+ const [styleSheet, setStyleSheet] = useState(userConfig.config.stylesheet);
+ const [styleSheetRefresh, setStyleSheetRefresh] = useState(false);
+ const [pageSize, setPageSize] = useState(userConfig.config.page_size);
+
+ useEffect(() => {
+ (async () => {
+ setStyleSheet(userConfig.config.stylesheet);
+ setPageSize(userConfig.config.page_size);
+ })();
+ }, [userConfig.config.page_size, userConfig.config.stylesheet]);
+
+ const handleStyleSheetChange = async (selectedStyleSheet: ColourVariants) => {
+ setPartialConfig({ stylesheet: selectedStyleSheet });
+ setStyleSheet(selectedStyleSheet);
+ setStyleSheetRefresh(true);
+ };
+
+ const handlePageSizeChange = async () => {
+ setPartialConfig({ page_size: pageSize });
+ };
+
+ const handlePageRefresh = () => {
+ navigate(0);
+ setStyleSheetRefresh(false);
+ };
+
+ return (
+ <>
+ TA | User Settings
+
+
+
+
+
+
User Configurations
+
+
+
+
Customize user Interface
+
+
+
Switch your color scheme
+
+
+ {
+ handleStyleSheetChange(event.target.value as ColourVariants);
+ }}
+ >
+ {Object.entries(ColourConstant).map(([key, value]) => {
+ return (
+
+ {key}
+
+ );
+ })}
+
+ {styleSheetRefresh && Refresh }
+
+
+
+
+
Archive view page size
+
+
+
{
+ setPageSize(Number(event.target.value));
+ }}
+ />
+
+ {userConfig.config.page_size !== pageSize && (
+ <>
+ Update
+ setPageSize(userConfig.config.page_size)}>
+ Cancel
+
+ >
+ )}
+
+
+
+
+
+ {isAdmin && (
+ <>
+
+
User Management
+
+ Access the admin interface for basic user management functionality like adding and
+ deleting users, changing passwords and more.
+
+
+
+
+
+ >
+ )}
+
+ >
+ );
+};
+
+export default SettingsUser;
diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx
index 562746b6..07a5426c 100644
--- a/frontend/src/pages/Video.tsx
+++ b/frontend/src/pages/Video.tsx
@@ -1,598 +1,598 @@
-import { Link, useNavigate, useParams } from 'react-router-dom';
-import loadVideoById from '../api/loader/loadVideoById';
-import { Fragment, useEffect, useState } from 'react';
-import { ConfigType, VideoType } from './Home';
-import VideoPlayer from '../components/VideoPlayer';
-import iconEye from '/img/icon-eye.svg';
-import iconThumb from '/img/icon-thumb.svg';
-import iconStarFull from '/img/icon-star-full.svg';
-import iconStarEmpty from '/img/icon-star-empty.svg';
-import iconStarHalf from '/img/icon-star-half.svg';
-import iconClose from '/img/icon-close.svg';
-import iconUnseen from '/img/icon-unseen.svg';
-import iconSeen from '/img/icon-seen.svg';
-import Routes from '../configuration/routes/RouteList';
-import Linkify from '../components/Linkify';
-import loadSimmilarVideosById from '../api/loader/loadSimmilarVideosById';
-import VideoList from '../components/VideoList';
-import updateWatchedState from '../api/actions/updateWatchedState';
-import humanFileSize from '../functions/humanFileSize';
-import ScrollToTopOnNavigate from '../components/ScrollToTop';
-import ChannelOverview from '../components/ChannelOverview';
-import deleteVideo from '../api/actions/deleteVideo';
-import capitalizeFirstLetter from '../functions/capitalizeFirstLetter';
-import formatDate from '../functions/formatDates';
-import formatNumbers from '../functions/formatNumbers';
-import queueReindex from '../api/actions/queueReindex';
-import GoogleCast from '../components/GoogleCast';
-import WatchedCheckBox from '../components/WatchedCheckBox';
-import convertStarRating from '../functions/convertStarRating';
-import loadPlaylistList from '../api/loader/loadPlaylistList';
-import { PlaylistsResponseType } from './Playlists';
-import PaginationDummy from '../components/PaginationDummy';
-import updateCustomPlaylist from '../api/actions/updateCustomPlaylist';
-import { PlaylistType } from './Playlist';
-import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
-import CommentBox, { CommentsType } from '../components/CommentBox';
-import Button from '../components/Button';
-import getApiUrl from '../configuration/getApiUrl';
-import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav';
-import loadIsAdmin from '../functions/getIsAdmin';
-
-const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
- return playlist.playlist_entries.some(entry => {
- return entry.youtube_id === videoId;
- });
-};
-
-type VideoParams = {
- videoId: string;
-};
-
-type PlaylistNavPreviousItemType = {
- youtube_id: string;
- vid_thumb: string;
- idx: number;
- title: string;
-};
-
-type PlaylistNavNextItemType = {
- youtube_id: string;
- vid_thumb: string;
- idx: number;
- title: string;
-};
-
-type PlaylistNavItemType = {
- playlist_meta: {
- current_idx: string;
- playlist_id: string;
- playlist_name: string;
- playlist_channel: string;
- };
- playlist_previous: PlaylistNavPreviousItemType;
- playlist_next: PlaylistNavNextItemType;
-};
-
-type PlaylistNavType = PlaylistNavItemType[];
-
-export type SponsorBlockSegmentType = {
- category: string;
- actionType: string;
- segment: number[];
- UUID: string;
- videoDuration: number;
- locked: number;
- votes: number;
-};
-
-export type SponsorBlockType = {
- last_refresh: number;
- has_unlocked: boolean;
- is_enabled: boolean;
- segments: SponsorBlockSegmentType[];
- message?: string;
-};
-
-export type SimilarVideosResponseType = {
- data: VideoType[];
- config: ConfigType;
-};
-
-export type VideoResponseType = {
- data: VideoType;
- config: ConfigType;
-};
-
-type CommentsResponseType = {
- data: CommentsType[];
- config: ConfigType;
-};
-
-export type VideoCommentsResponseType = {
- data: VideoType;
- config: ConfigType;
- playlist_nav: PlaylistNavType;
-};
-
-const Video = () => {
- const { videoId } = useParams() as VideoParams;
- const navigate = useNavigate();
- const isAdmin = loadIsAdmin();
-
- const [loading, setLoading] = useState(false);
- const [videoEnded, setVideoEnded] = useState(false);
- const [playlistAutoplay, setPlaylistAutoplay] = useState(
- localStorage.getItem('playlistAutoplay') === 'true',
- );
- const [playlistIdForAutoplay, setPlaylistIDForAutoplay] = useState(
- localStorage.getItem('playlistIdForAutoplay') ?? '',
- );
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [showAddToPlaylist, setShowAddToPlaylist] = useState(false);
- const [refreshVideoList, setRefreshVideoList] = useState(false);
- const [reindex, setReindex] = useState(false);
-
- const [videoResponse, setVideoResponse] = useState();
- const [simmilarVideos, setSimmilarVideos] = useState();
- const [videoPlaylistNav, setVideoPlaylistNav] = useState();
- const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState();
- const [commentsResponse, setCommentsResponse] = useState();
-
- useEffect(() => {
- (async () => {
- setLoading(true);
-
- const videoResponse = await loadVideoById(videoId);
- const simmilarVideosResponse = await loadSimmilarVideosById(videoId);
- const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' });
- const commentsResponse = await loadCommentsbyVideoId(videoId);
- const videoNavResponse = await loadVideoNav(videoId);
-
- setVideoResponse(videoResponse);
- setSimmilarVideos(simmilarVideosResponse);
- setVideoPlaylistNav(videoNavResponse);
- setCustomPlaylistsResponse(customPlaylistsResponse);
- setCommentsResponse(commentsResponse);
- setRefreshVideoList(false);
-
- setLoading(false);
- })();
- }, [videoId, refreshVideoList]);
-
- useEffect(() => {
- localStorage.setItem('playlistAutoplay', playlistAutoplay.toString());
-
- if (!playlistAutoplay) {
- localStorage.setItem('playlistIdForAutoplay', '');
-
- return;
- }
-
- localStorage.setItem('playlistIdForAutoplay', playlistIdForAutoplay || '');
- }, [playlistAutoplay, playlistIdForAutoplay]);
-
- useEffect(() => {
- if (videoEnded && playlistAutoplay) {
- const playlist = videoPlaylistNav?.find(playlist => {
- return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
- });
-
- if (playlist) {
- const nextYoutubeId = playlist.playlist_next?.youtube_id;
-
- if (nextYoutubeId) {
- setVideoEnded(false);
- navigate(Routes.Video(nextYoutubeId));
- }
- }
- }
- }, [videoEnded, playlistAutoplay]);
-
- if (videoResponse === undefined) {
- return [];
- }
-
- const video = videoResponse.data;
- const watched = videoResponse.data.player.watched;
- const config = videoResponse.config;
- const playlistNav = videoPlaylistNav;
- const sponsorBlock = videoResponse.data.sponsorblock;
- const customPlaylists = customPlaylistsResponse?.data;
- const starRating = convertStarRating(video?.stats?.average_rating);
- const comments = commentsResponse?.data;
-
- console.log('playlistNav', playlistNav);
-
- const cast = config.enable_cast;
-
- return (
- <>
- {`TA | ${video.title}`}
-
-
- {!loading && (
- {
- setVideoEnded(true);
- }}
- />
- )}
-
-
-
- {cast && (
- {
- setRefreshVideoList(true);
- }}
- />
- )}
- {video.title}
-
-
-
-
-
-
-
Published: {formatDate(video.published)}
-
Last refreshed: {formatDate(video.vid_last_refresh)}
-
- Watched:
- {
- await updateWatchedState({
- id: videoId,
- is_watched: status,
- });
- }}
- onDone={() => {
- setRefreshVideoList(true);
- }}
- />
-
- {video.active && (
-
- Youtube:{' '}
-
- Active
-
-
- )}
- {!video.active &&
Youtube: Deactivated
}
-
-
-
-
-
- : {formatNumbers(video.stats.view_count)}
-
-
- : {formatNumbers(video.stats.like_count)}
-
- {video.stats.dislike_count > 0 && (
-
- :{' '}
- {video.stats.dislike_count}
-
- )}
- {video?.stats && starRating && (
-
- {starRating?.map?.((star, index) => {
- if (star === 'full') {
- return
;
- }
-
- if (star === 'half') {
- return
;
- }
-
- return
;
- })}
-
- )}
-
-
-
-
-
-
- {reindex &&
Reindex scheduled
}
- {!reindex && (
- <>
- {isAdmin && (
-
- {
- await queueReindex(video.youtube_id, 'video');
- setReindex(true);
- }}
- />
-
- )}
- >
- )}
-
-
- {' '}
- {isAdmin && (
- <>
- {!showDeleteConfirm && (
-
setShowDeleteConfirm(!showDeleteConfirm)}
- />
- )}
-
- {showDeleteConfirm && (
-
- Are you sure?
- {
- await deleteVideo(videoId);
- navigate(Routes.Channel(video.channel.channel_id));
- }}
- />{' '}
- setShowDeleteConfirm(!showDeleteConfirm)}
- />
-
- )}
- >
- )}{' '}
- {!showAddToPlaylist && (
- {
- setShowAddToPlaylist(true);
- }}
- />
- )}
- {showAddToPlaylist && (
- <>
-
-
{
- setShowAddToPlaylist(false);
- }}
- />
-
Add video to...
-
- {customPlaylists?.map(playlist => {
- return (
-
{
- if (isInPlaylist(videoId, playlist)) {
- await updateCustomPlaylist('remove', playlist.playlist_id, videoId);
- } else {
- await updateCustomPlaylist('create', playlist.playlist_id, videoId);
- }
-
- setRefreshVideoList(true);
- }}
- >
- {isInPlaylist(videoId, playlist) && (
-
- )}
-
- {!isInPlaylist(videoId, playlist) && (
-
- )}
-
- {playlist.playlist_name}
-
- );
- })}
-
-
- Create playlist
-
-
- >
- )}
-
-
-
- {video.media_size &&
File size: {humanFileSize(video.media_size)}
}
-
- {video.streams &&
- video.streams.map(stream => {
- return (
-
- {capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
- {humanFileSize(stream.bitrate)}/s
- {stream.width && (
- <>
- | {stream.width}x{stream.height}
- >
- )}{' '}
-
- );
- })}
-
-
- {video.tags && video.tags.length > 0 && (
-
-
- {video.tags.map(tag => {
- return (
-
- {tag}
-
- );
- })}
-
-
- )}
-
- {video.description && (
-
-
- {video.description}
-
-
-
setDescriptionExpanded(!descriptionExpanded)}
- />
-
- )}
-
- {playlistNav && (
- <>
- {playlistNav.map(playlistItem => {
- return (
-
-
-
- Playlist [{playlistItem.playlist_meta.current_idx + 1}
- ]: {playlistItem.playlist_meta.playlist_name}
-
-
-
-
-
Autoplay:
-
- {
- if (!playlistAutoplay) {
- setPlaylistIDForAutoplay(playlistItem.playlist_meta.playlist_id);
- }
-
- setPlaylistAutoplay(!playlistAutoplay);
- }}
- type="checkbox"
- />
- {!playlistAutoplay && (
-
- Off
-
- )}
- {playlistAutoplay && (
-
- On
-
- )}
-
-
-
-
-
- {playlistItem.playlist_previous && (
- <>
-
-
-
-
-
Previous:
-
-
- [{playlistItem.playlist_previous.idx + 1}]{' '}
- {playlistItem.playlist_previous.title}
-
-
-
- >
- )}
-
-
- {playlistItem.playlist_next && (
- <>
-
-
Next:
-
-
- [{playlistItem.playlist_next.idx + 1}]{' '}
- {playlistItem.playlist_next.title}
-
-
-
-
-
-
- >
- )}
-
-
-
- );
- })}
- >
- )}
-
-
-
- {video.comment_count == 0 && (
-
- Video has no comments
-
- )}
-
- {video.comment_count && (
-
-
Comments: {video.comment_count}
-
-
- )}
-
-
-
-
- >
- );
-};
-
-export default Video;
+import { Link, useNavigate, useParams } from 'react-router-dom';
+import loadVideoById from '../api/loader/loadVideoById';
+import { Fragment, useEffect, useState } from 'react';
+import { ConfigType, VideoType } from './Home';
+import VideoPlayer from '../components/VideoPlayer';
+import iconEye from '/img/icon-eye.svg';
+import iconThumb from '/img/icon-thumb.svg';
+import iconStarFull from '/img/icon-star-full.svg';
+import iconStarEmpty from '/img/icon-star-empty.svg';
+import iconStarHalf from '/img/icon-star-half.svg';
+import iconClose from '/img/icon-close.svg';
+import iconUnseen from '/img/icon-unseen.svg';
+import iconSeen from '/img/icon-seen.svg';
+import Routes from '../configuration/routes/RouteList';
+import Linkify from '../components/Linkify';
+import loadSimmilarVideosById from '../api/loader/loadSimmilarVideosById';
+import VideoList from '../components/VideoList';
+import updateWatchedState from '../api/actions/updateWatchedState';
+import humanFileSize from '../functions/humanFileSize';
+import ScrollToTopOnNavigate from '../components/ScrollToTop';
+import ChannelOverview from '../components/ChannelOverview';
+import deleteVideo from '../api/actions/deleteVideo';
+import capitalizeFirstLetter from '../functions/capitalizeFirstLetter';
+import formatDate from '../functions/formatDates';
+import formatNumbers from '../functions/formatNumbers';
+import queueReindex from '../api/actions/queueReindex';
+import GoogleCast from '../components/GoogleCast';
+import WatchedCheckBox from '../components/WatchedCheckBox';
+import convertStarRating from '../functions/convertStarRating';
+import loadPlaylistList from '../api/loader/loadPlaylistList';
+import { PlaylistsResponseType } from './Playlists';
+import PaginationDummy from '../components/PaginationDummy';
+import updateCustomPlaylist from '../api/actions/updateCustomPlaylist';
+import { PlaylistType } from './Playlist';
+import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
+import CommentBox, { CommentsType } from '../components/CommentBox';
+import Button from '../components/Button';
+import getApiUrl from '../configuration/getApiUrl';
+import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav';
+import loadIsAdmin from '../functions/getIsAdmin';
+
+const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
+ return playlist.playlist_entries.some(entry => {
+ return entry.youtube_id === videoId;
+ });
+};
+
+type VideoParams = {
+ videoId: string;
+};
+
+type PlaylistNavPreviousItemType = {
+ youtube_id: string;
+ vid_thumb: string;
+ idx: number;
+ title: string;
+};
+
+type PlaylistNavNextItemType = {
+ youtube_id: string;
+ vid_thumb: string;
+ idx: number;
+ title: string;
+};
+
+type PlaylistNavItemType = {
+ playlist_meta: {
+ current_idx: string;
+ playlist_id: string;
+ playlist_name: string;
+ playlist_channel: string;
+ };
+ playlist_previous: PlaylistNavPreviousItemType;
+ playlist_next: PlaylistNavNextItemType;
+};
+
+type PlaylistNavType = PlaylistNavItemType[];
+
+export type SponsorBlockSegmentType = {
+ category: string;
+ actionType: string;
+ segment: number[];
+ UUID: string;
+ videoDuration: number;
+ locked: number;
+ votes: number;
+};
+
+export type SponsorBlockType = {
+ last_refresh: number;
+ has_unlocked: boolean;
+ is_enabled: boolean;
+ segments: SponsorBlockSegmentType[];
+ message?: string;
+};
+
+export type SimilarVideosResponseType = {
+ data: VideoType[];
+ config: ConfigType;
+};
+
+export type VideoResponseType = {
+ data: VideoType;
+ config: ConfigType;
+};
+
+type CommentsResponseType = {
+ data: CommentsType[];
+ config: ConfigType;
+};
+
+export type VideoCommentsResponseType = {
+ data: VideoType;
+ config: ConfigType;
+ playlist_nav: PlaylistNavType;
+};
+
+const Video = () => {
+ const { videoId } = useParams() as VideoParams;
+ const navigate = useNavigate();
+ const isAdmin = loadIsAdmin();
+
+ const [loading, setLoading] = useState(false);
+ const [videoEnded, setVideoEnded] = useState(false);
+ const [playlistAutoplay, setPlaylistAutoplay] = useState(
+ localStorage.getItem('playlistAutoplay') === 'true',
+ );
+ const [playlistIdForAutoplay, setPlaylistIDForAutoplay] = useState(
+ localStorage.getItem('playlistIdForAutoplay') ?? '',
+ );
+ const [descriptionExpanded, setDescriptionExpanded] = useState(false);
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const [showAddToPlaylist, setShowAddToPlaylist] = useState(false);
+ const [refreshVideoList, setRefreshVideoList] = useState(false);
+ const [reindex, setReindex] = useState(false);
+
+ const [videoResponse, setVideoResponse] = useState();
+ const [simmilarVideos, setSimmilarVideos] = useState();
+ const [videoPlaylistNav, setVideoPlaylistNav] = useState();
+ const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState();
+ const [commentsResponse, setCommentsResponse] = useState();
+
+ useEffect(() => {
+ (async () => {
+ setLoading(true);
+
+ const videoResponse = await loadVideoById(videoId);
+ const simmilarVideosResponse = await loadSimmilarVideosById(videoId);
+ const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' });
+ const commentsResponse = await loadCommentsbyVideoId(videoId);
+ const videoNavResponse = await loadVideoNav(videoId);
+
+ setVideoResponse(videoResponse);
+ setSimmilarVideos(simmilarVideosResponse);
+ setVideoPlaylistNav(videoNavResponse);
+ setCustomPlaylistsResponse(customPlaylistsResponse);
+ setCommentsResponse(commentsResponse);
+ setRefreshVideoList(false);
+
+ setLoading(false);
+ })();
+ }, [videoId, refreshVideoList]);
+
+ useEffect(() => {
+ localStorage.setItem('playlistAutoplay', playlistAutoplay.toString());
+
+ if (!playlistAutoplay) {
+ localStorage.setItem('playlistIdForAutoplay', '');
+
+ return;
+ }
+
+ localStorage.setItem('playlistIdForAutoplay', playlistIdForAutoplay || '');
+ }, [playlistAutoplay, playlistIdForAutoplay]);
+
+ useEffect(() => {
+ if (videoEnded && playlistAutoplay) {
+ const playlist = videoPlaylistNav?.find(playlist => {
+ return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
+ });
+
+ if (playlist) {
+ const nextYoutubeId = playlist.playlist_next?.youtube_id;
+
+ if (nextYoutubeId) {
+ setVideoEnded(false);
+ navigate(Routes.Video(nextYoutubeId));
+ }
+ }
+ }
+ }, [videoEnded, playlistAutoplay]);
+
+ if (videoResponse === undefined) {
+ return [];
+ }
+
+ const video = videoResponse.data;
+ const watched = videoResponse.data.player.watched;
+ const config = videoResponse.config;
+ const playlistNav = videoPlaylistNav;
+ const sponsorBlock = videoResponse.data.sponsorblock;
+ const customPlaylists = customPlaylistsResponse?.data;
+ const starRating = convertStarRating(video?.stats?.average_rating);
+ const comments = commentsResponse?.data;
+
+ console.log('playlistNav', playlistNav);
+
+ const cast = config.enable_cast;
+
+ return (
+ <>
+ {`TA | ${video.title}`}
+
+
+ {!loading && (
+ {
+ setVideoEnded(true);
+ }}
+ />
+ )}
+
+
+
+ {cast && (
+ {
+ setRefreshVideoList(true);
+ }}
+ />
+ )}
+ {video.title}
+
+
+
+
+
+
+
Published: {formatDate(video.published)}
+
Last refreshed: {formatDate(video.vid_last_refresh)}
+
+ Watched:
+ {
+ await updateWatchedState({
+ id: videoId,
+ is_watched: status,
+ });
+ }}
+ onDone={() => {
+ setRefreshVideoList(true);
+ }}
+ />
+
+ {video.active && (
+
+ Youtube:{' '}
+
+ Active
+
+
+ )}
+ {!video.active &&
Youtube: Deactivated
}
+
+
+
+
+
+ : {formatNumbers(video.stats.view_count)}
+
+
+ : {formatNumbers(video.stats.like_count)}
+
+ {video.stats.dislike_count > 0 && (
+
+ :{' '}
+ {video.stats.dislike_count}
+
+ )}
+ {video?.stats && starRating && (
+
+ {starRating?.map?.((star, index) => {
+ if (star === 'full') {
+ return
;
+ }
+
+ if (star === 'half') {
+ return
;
+ }
+
+ return
;
+ })}
+
+ )}
+
+
+
+
+
+
+ {reindex &&
Reindex scheduled
}
+ {!reindex && (
+ <>
+ {isAdmin && (
+
+ {
+ await queueReindex(video.youtube_id, 'video');
+ setReindex(true);
+ }}
+ />
+
+ )}
+ >
+ )}
+
+
+ {' '}
+ {isAdmin && (
+ <>
+ {!showDeleteConfirm && (
+
setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+ )}
+
+ {showDeleteConfirm && (
+
+ Are you sure?
+ {
+ await deleteVideo(videoId);
+ navigate(Routes.Channel(video.channel.channel_id));
+ }}
+ />{' '}
+ setShowDeleteConfirm(!showDeleteConfirm)}
+ />
+
+ )}
+ >
+ )}{' '}
+ {!showAddToPlaylist && (
+ {
+ setShowAddToPlaylist(true);
+ }}
+ />
+ )}
+ {showAddToPlaylist && (
+ <>
+
+
{
+ setShowAddToPlaylist(false);
+ }}
+ />
+
Add video to...
+
+ {customPlaylists?.map(playlist => {
+ return (
+
{
+ if (isInPlaylist(videoId, playlist)) {
+ await updateCustomPlaylist('remove', playlist.playlist_id, videoId);
+ } else {
+ await updateCustomPlaylist('create', playlist.playlist_id, videoId);
+ }
+
+ setRefreshVideoList(true);
+ }}
+ >
+ {isInPlaylist(videoId, playlist) && (
+
+ )}
+
+ {!isInPlaylist(videoId, playlist) && (
+
+ )}
+
+ {playlist.playlist_name}
+
+ );
+ })}
+
+
+ Create playlist
+
+
+ >
+ )}
+
+
+
+ {video.media_size &&
File size: {humanFileSize(video.media_size)}
}
+
+ {video.streams &&
+ video.streams.map(stream => {
+ return (
+
+ {capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
+ {humanFileSize(stream.bitrate)}/s
+ {stream.width && (
+ <>
+ | {stream.width}x{stream.height}
+ >
+ )}{' '}
+
+ );
+ })}
+
+
+ {video.tags && video.tags.length > 0 && (
+
+
+ {video.tags.map(tag => {
+ return (
+
+ {tag}
+
+ );
+ })}
+
+
+ )}
+
+ {video.description && (
+
+
+ {video.description}
+
+
+
setDescriptionExpanded(!descriptionExpanded)}
+ />
+
+ )}
+
+ {playlistNav && (
+ <>
+ {playlistNav.map(playlistItem => {
+ return (
+
+
+
+ Playlist [{playlistItem.playlist_meta.current_idx + 1}
+ ]: {playlistItem.playlist_meta.playlist_name}
+
+
+
+
+
Autoplay:
+
+ {
+ if (!playlistAutoplay) {
+ setPlaylistIDForAutoplay(playlistItem.playlist_meta.playlist_id);
+ }
+
+ setPlaylistAutoplay(!playlistAutoplay);
+ }}
+ type="checkbox"
+ />
+ {!playlistAutoplay && (
+
+ Off
+
+ )}
+ {playlistAutoplay && (
+
+ On
+
+ )}
+
+
+
+
+
+ {playlistItem.playlist_previous && (
+ <>
+
+
+
+
+
Previous:
+
+
+ [{playlistItem.playlist_previous.idx + 1}]{' '}
+ {playlistItem.playlist_previous.title}
+
+
+
+ >
+ )}
+
+
+ {playlistItem.playlist_next && (
+ <>
+
+
Next:
+
+
+ [{playlistItem.playlist_next.idx + 1}]{' '}
+ {playlistItem.playlist_next.title}
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ );
+ })}
+ >
+ )}
+
+
+
+ {video.comment_count == 0 && (
+
+ Video has no comments
+
+ )}
+
+ {video.comment_count && (
+
+
Comments: {video.comment_count}
+
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Video;
diff --git a/frontend/src/stores/AuthDataStore.ts b/frontend/src/stores/AuthDataStore.ts
index 49f484c9..d90c8537 100644
--- a/frontend/src/stores/AuthDataStore.ts
+++ b/frontend/src/stores/AuthDataStore.ts
@@ -6,7 +6,7 @@ interface AuthState {
setAuth: (auth: AuthenticationType) => void;
}
-export const useAuthStore = create((set) => ({
+export const useAuthStore = create(set => ({
auth: null,
- setAuth: (auth) => set({ auth }),
+ setAuth: auth => set({ auth }),
}));
diff --git a/frontend/src/stores/UserConfigStore.ts b/frontend/src/stores/UserConfigStore.ts
index ee00a67a..632f97f5 100644
--- a/frontend/src/stores/UserConfigStore.ts
+++ b/frontend/src/stores/UserConfigStore.ts
@@ -7,8 +7,7 @@ interface UserConfigState {
setPartialConfig: (userConfig: Partial) => void;
}
-export const useUserConfigStore = create((set) => ({
-
+export const useUserConfigStore = create(set => ({
userConfig: {
id: 0,
name: '',
@@ -30,15 +29,16 @@ export const useUserConfigStore = create((set) => ({
hide_watched: false,
show_ignored_only: false,
show_subed_only: false,
- }
+ },
},
- setUserConfig: (userConfig) => set({ userConfig }),
+ setUserConfig: userConfig => set({ userConfig }),
setPartialConfig: async (userConfig: Partial) => {
const userConfigResponse = await updateUserConfig(userConfig);
- set((state) => ({
- userConfig: state.userConfig ? { ...state.userConfig, config: userConfigResponse } : state.userConfig,
+ set(state => ({
+ userConfig: state.userConfig
+ ? { ...state.userConfig, config: userConfigResponse }
+ : state.userConfig,
}));
- }
-
-}))
+ },
+}));