Refac APIClient to return data, error and status as object

This commit is contained in:
MerlinScheurer
2025-03-20 20:30:08 +01:00
parent 52083e6fb7
commit edcede5de6
67 changed files with 660 additions and 544 deletions

View File

@@ -1,8 +1,8 @@
import APIClient from '../../functions/APIClient';
import { CookieStateType } from '../loader/loadCookie';
const deleteCookie = async (): Promise<CookieStateType> => {
return APIClient('/api/appsettings/cookie/', {
const deleteCookie = async () => {
return APIClient<CookieStateType>('/api/appsettings/cookie/', {
method: 'DELETE',
});
};

View File

@@ -1,8 +1,8 @@
import APIClient from '../../functions/APIClient';
import { CookieStateType } from '../loader/loadCookie';
const updateCookie = async (cookie: string): Promise<CookieStateType> => {
return APIClient('/api/appsettings/cookie/', {
const updateCookie = async (cookie: string) => {
return APIClient<CookieStateType>('/api/appsettings/cookie/', {
method: 'PUT',
body: { cookie },
});

View File

@@ -25,8 +25,8 @@ export type UserConfigType = {
show_help_text: boolean;
};
const updateUserConfig = async (config: Partial<UserConfigType>): Promise<UserConfigType> => {
return APIClient('/api/user/me/', {
const updateUserConfig = async (config: Partial<UserConfigType>) => {
return APIClient<UserConfigType>('/api/user/me/', {
method: 'POST',
body: config,
});

View File

@@ -14,11 +14,8 @@ type VideoProgressProp = {
currentProgress: number;
};
const updateVideoProgressById = async ({
youtubeId,
currentProgress,
}: VideoProgressProp): Promise<VideoProgressResponseType> => {
return APIClient(`/api/video/${youtubeId}/progress/`, {
const updateVideoProgressById = async ({ youtubeId, currentProgress }: VideoProgressProp) => {
return APIClient<VideoProgressResponseType>(`/api/video/${youtubeId}/progress/`, {
method: 'POST',
body: { position: currentProgress },
});

View File

@@ -1,8 +1,8 @@
import APIClient from '../../functions/APIClient';
import { CookieStateType } from '../loader/loadCookie';
const validateCookie = async (): Promise<CookieStateType> => {
return APIClient('/api/appsettings/cookie/', {
const validateCookie = async () => {
return APIClient<CookieStateType>('/api/appsettings/cookie/', {
method: 'POST',
});
};

View File

@@ -4,8 +4,8 @@ type ApiTokenResponse = {
token: string;
};
const loadApiToken = async (): Promise<ApiTokenResponse> => {
return APIClient('/api/appsettings/token/');
const loadApiToken = async () => {
return APIClient<ApiTokenResponse>('/api/appsettings/token/');
};
export default loadApiToken;

View File

@@ -19,8 +19,8 @@ export type AppriseNotificationType = {
};
};
const loadAppriseNotification = async (): Promise<AppriseNotificationType> => {
return APIClient('/api/task/notification/');
const loadAppriseNotification = async () => {
return APIClient<AppriseNotificationType>('/api/task/notification/');
};
export default loadAppriseNotification;

View File

@@ -33,8 +33,8 @@ export type AppSettingsConfigType = {
};
};
const loadAppsettingsConfig = async (): Promise<AppSettingsConfigType> => {
return APIClient('/api/appsettings/config/');
const loadAppsettingsConfig = async () => {
return APIClient<AppSettingsConfigType>('/api/appsettings/config/');
};
export default loadAppsettingsConfig;

View File

@@ -1,7 +1,17 @@
import APIClient from '../../functions/APIClient';
type Backup = {
filename: string;
file_path: string;
file_size: number;
timestamp: string;
reason: string;
};
export type BackupListType = Backup[];
const loadBackupList = async () => {
return APIClient('/api/appsettings/backup/');
return APIClient<BackupListType>('/api/appsettings/backup/');
};
export default loadBackupList;

View File

@@ -13,8 +13,8 @@ export type ChannelAggsType = {
};
};
const loadChannelAggs = async (channelId: string): Promise<ChannelAggsType> => {
return APIClient(`/api/channel/${channelId}/aggs/`);
const loadChannelAggs = async (channelId: string) => {
return APIClient<ChannelAggsType>(`/api/channel/${channelId}/aggs/`);
};
export default loadChannelAggs;

View File

@@ -1,8 +1,10 @@
import APIClient from '../../functions/APIClient';
import { ChannelResponseType } from '../../pages/ChannelBase';
import { ChannelType } from '../../pages/Channels';
const loadChannelById = async (youtubeChannelId: string): Promise<ChannelResponseType> => {
return APIClient(`/api/channel/${youtubeChannelId}/`);
export type ChannelResponseType = ChannelType;
const loadChannelById = async (youtubeChannelId: string) => {
return APIClient<ChannelResponseType>(`/api/channel/${youtubeChannelId}/`);
};
export default loadChannelById;

View File

@@ -1,4 +1,13 @@
import { PaginationType } from '../../components/Pagination';
import APIClient from '../../functions/APIClient';
import { ChannelType } from '../../pages/Channels';
import { ConfigType } from '../../pages/Home';
export type ChannelsListResponse = {
data: ChannelType[];
paginate: PaginationType;
config?: ConfigType;
};
const loadChannelList = async (page: number, showSubscribed: boolean) => {
const searchParams = new URLSearchParams();
@@ -8,7 +17,7 @@ const loadChannelList = async (page: number, showSubscribed: boolean) => {
const endpoint = `/api/channel/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
return APIClient(endpoint);
return APIClient<ChannelsListResponse>(endpoint);
};
export default loadChannelList;

View File

@@ -7,8 +7,8 @@ export type ChannelNavResponseType = {
has_pending: boolean;
};
const loadChannelNav = async (youtubeChannelId: string): Promise<ChannelNavResponseType> => {
return APIClient(`/api/channel/${youtubeChannelId}/nav/`);
const loadChannelNav = async (youtubeChannelId: string) => {
return APIClient<ChannelNavResponseType>(`/api/channel/${youtubeChannelId}/nav/`);
};
export default loadChannelNav;

View File

@@ -1,7 +1,10 @@
import { CommentsType } from '../../components/CommentBox';
import APIClient from '../../functions/APIClient';
export type CommentsResponseType = CommentsType[];
const loadCommentsbyVideoId = async (youtubeId: string) => {
return APIClient(`/api/video/${youtubeId}/comment/`);
return APIClient<CommentsResponseType>(`/api/video/${youtubeId}/comment/`);
};
export default loadCommentsbyVideoId;

View File

@@ -7,8 +7,8 @@ export type CookieStateType = {
validated_str?: string;
};
const loadCookie = async (): Promise<CookieStateType> => {
return APIClient('/api/appsettings/cookie/');
const loadCookie = async () => {
return APIClient<CookieStateType>('/api/appsettings/cookie/');
};
export default loadCookie;

View File

@@ -12,10 +12,10 @@ export type DownloadAggsType = {
buckets: DownloadAggsBucket[];
};
const loadDownloadAggs = async (showIgnored: boolean): Promise<DownloadAggsType> => {
const loadDownloadAggs = async (showIgnored: boolean) => {
const searchParams = new URLSearchParams();
searchParams.append('filter', showIgnored ? 'ignore' : 'pending');
return APIClient(
return APIClient<DownloadAggsType>(
`/api/download/aggs/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`,
);
};

View File

@@ -1,11 +1,7 @@
import APIClient from '../../functions/APIClient';
import { DownloadResponseType } from '../../pages/Download';
const loadDownloadQueue = async (
page: number,
channelId: string | null,
showIgnored: boolean,
): Promise<DownloadResponseType> => {
const loadDownloadQueue = async (page: number, channelId: string | null, showIgnored: boolean) => {
const searchParams = new URLSearchParams();
if (page) searchParams.append('page', page.toString());
@@ -14,7 +10,7 @@ const loadDownloadQueue = async (
const endpoint = `/api/download/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
return APIClient(endpoint);
return APIClient<DownloadResponseType>(endpoint);
};
export default loadDownloadQueue;

View File

@@ -2,6 +2,19 @@ import APIClient from '../../functions/APIClient';
export type NotificationPages = 'download' | 'settings' | 'channel' | 'all';
type NotificationType = {
title: string;
group: string;
api_stop: boolean;
level: string;
id: string;
command: boolean | string;
messages: string[];
progress: number;
};
export type NotificationResponseType = NotificationType[];
const loadNotifications = async (pageName: NotificationPages, includeReindex = false) => {
const searchParams = new URLSearchParams();
@@ -10,7 +23,7 @@ const loadNotifications = async (pageName: NotificationPages, includeReindex = f
}
const endpoint = `/api/notification/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
return APIClient(endpoint);
return APIClient<NotificationResponseType>(endpoint);
};
export default loadNotifications;

View File

@@ -26,8 +26,8 @@ export type PlaylistType = {
export type PlaylistResponseType = PlaylistType;
const loadPlaylistById = async (playlistId: string | undefined): Promise<PlaylistResponseType> => {
return APIClient(`/api/playlist/${playlistId}/`);
const loadPlaylistById = async (playlistId: string | undefined) => {
return APIClient<PlaylistResponseType>(`/api/playlist/${playlistId}/`);
};
export default loadPlaylistById;

View File

@@ -1,12 +1,19 @@
import { PaginationType } from '../../components/Pagination';
import APIClient from '../../functions/APIClient';
import { PlaylistType } from './loadPlaylistById';
type PlaylistType = 'regular' | 'custom';
export type PlaylistsResponseType = {
data?: PlaylistType[];
paginate?: PaginationType;
};
type PlaylistCategoryType = 'regular' | 'custom';
type LoadPlaylistListProps = {
channel?: string;
page?: number | undefined;
subscribed?: boolean;
type?: PlaylistType;
type?: PlaylistCategoryType;
};
const loadPlaylistList = async ({ channel, page, subscribed, type }: LoadPlaylistListProps) => {
@@ -18,7 +25,7 @@ const loadPlaylistList = async ({ channel, page, subscribed, type }: LoadPlaylis
if (type) searchParams.append('type', type);
const endpoint = `/api/playlist/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
return APIClient(endpoint);
return APIClient<PlaylistsResponseType>(endpoint);
};
export default loadPlaylistList;

View File

@@ -13,8 +13,8 @@ type ScheduleType = {
export type ScheduleResponseType = ScheduleType[];
const loadSchedule = async (): Promise<ScheduleResponseType> => {
return APIClient('/api/task/schedule/');
const loadSchedule = async () => {
return APIClient<ScheduleResponseType>('/api/task/schedule/');
};
export default loadSchedule;

View File

@@ -1,7 +1,22 @@
import APIClient from '../../functions/APIClient';
import { ChannelType } from '../../pages/Channels';
import { VideoType } from '../../pages/Home';
import { PlaylistType } from './loadPlaylistById';
type SearchResultType = {
video_results: VideoType[];
channel_results: ChannelType[];
playlist_results: PlaylistType[];
fulltext_results: [];
};
export type SearchResultsType = {
results: SearchResultType;
queryType: string;
};
const loadSearch = async (query: string) => {
return APIClient(`/api/search/?query=${query}`);
return APIClient<SearchResultsType>(`/api/search/?query=${query}`);
};
export default loadSearch;

View File

@@ -0,0 +1,8 @@
import APIClient from '../../functions/APIClient';
import { VideoResponseType } from './loadVideoById';
const loadSimilarVideosById = async (youtubeId: string) => {
return APIClient<VideoResponseType[]>(`/api/video/${youtubeId}/similar/`);
};
export default loadSimilarVideosById;

View File

@@ -1,7 +0,0 @@
import APIClient from '../../functions/APIClient';
const loadSimmilarVideosById = async (youtubeId: string) => {
return APIClient(`/api/video/${youtubeId}/similar/`);
};
export default loadSimmilarVideosById;

View File

@@ -1,7 +1,24 @@
import APIClient from '../../functions/APIClient';
export type SnapshotType = {
id: string;
state: string;
es_version: string;
start_date: string;
end_date: string;
end_stamp: number;
duration_s: number;
};
export type SnapshotListType = {
next_exec: number;
next_exec_str: string;
expire_after: string;
snapshots?: SnapshotType[];
};
const loadSnapshots = async () => {
return APIClient('/api/appsettings/snapshot/');
return APIClient<SnapshotListType>('/api/appsettings/snapshot/');
};
export default loadSnapshots;

View File

@@ -2,11 +2,24 @@ import APIClient from '../../functions/APIClient';
type BiggestChannelsOrderType = 'doc_count' | 'duration' | 'media_size';
type BiggestChannelsType = {
id: string;
name: string;
doc_count: number;
duration: number;
duration_str: string;
media_size: number;
};
export type BiggestChannelsStatsType = BiggestChannelsType[];
const loadStatsBiggestChannels = async (order: BiggestChannelsOrderType) => {
const searchParams = new URLSearchParams();
searchParams.append('order', order);
return APIClient(`/api/stats/biggestchannels/?${searchParams.toString()}`);
return APIClient<BiggestChannelsStatsType>(
`/api/stats/biggestchannels/?${searchParams.toString()}`,
);
};
export default loadStatsBiggestChannels;

View File

@@ -1,7 +1,13 @@
import APIClient from '../../functions/APIClient';
export type ChannelStatsType = {
doc_count: number;
active_true: number;
subscribed_true: number;
};
const loadStatsChannel = async () => {
return APIClient('/api/stats/channel/');
return APIClient<ChannelStatsType>('/api/stats/channel/');
};
export default loadStatsChannel;

View File

@@ -1,7 +1,14 @@
import APIClient from '../../functions/APIClient';
export type DownloadStatsType = {
pending: number;
pending_videos: number;
pending_shorts: number;
pending_streams: number;
};
const loadStatsDownload = async () => {
return APIClient('/api/stats/download/');
return APIClient<DownloadStatsType>('/api/stats/download/');
};
export default loadStatsDownload;

View File

@@ -1,7 +1,15 @@
import APIClient from '../../functions/APIClient';
type DownloadHistoryType = {
date: string;
count: number;
media_size: number;
};
export type DownloadHistoryStatsType = DownloadHistoryType[];
const loadStatsDownloadHistory = async () => {
return APIClient('/api/stats/downloadhist/');
return APIClient<DownloadHistoryStatsType>('/api/stats/downloadhist/');
};
export default loadStatsDownloadHistory;

View File

@@ -1,7 +1,14 @@
import APIClient from '../../functions/APIClient';
export type PlaylistStatsType = {
doc_count: number;
active_false: number;
active_true: number;
subscribed_true: number;
};
const loadStatsPlaylist = async () => {
return APIClient('/api/stats/playlist/');
return APIClient<PlaylistStatsType>('/api/stats/playlist/');
};
export default loadStatsPlaylist;

View File

@@ -1,7 +1,44 @@
import APIClient from '../../functions/APIClient';
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;
};
};
const loadStatsVideo = async () => {
return APIClient('/api/stats/video/');
return APIClient<VideoStatsType>('/api/stats/video/');
};
export default loadStatsVideo;

View File

@@ -1,7 +1,27 @@
import APIClient from '../../functions/APIClient';
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;
};
};
const loadStatsWatchProgress = async () => {
return APIClient('/api/stats/watch/');
return APIClient<WatchProgressStatsType>('/api/stats/watch/');
};
export default loadStatsWatchProgress;

View File

@@ -10,8 +10,8 @@ export type UserAccountType = {
last_login: string;
};
const loadUserAccount = async (): Promise<UserAccountType> => {
return APIClient('/api/user/account/');
const loadUserAccount = async () => {
return APIClient<UserAccountType>('/api/user/account/');
};
export default loadUserAccount;

View File

@@ -1,8 +1,8 @@
import { UserConfigType } from '../actions/updateUserConfig';
import APIClient from '../../functions/APIClient';
const loadUserMeConfig = async (): Promise<UserConfigType> => {
return APIClient('/api/user/me/');
const loadUserMeConfig = async () => {
return APIClient<UserConfigType>('/api/user/me/');
};
export default loadUserMeConfig;

View File

@@ -1,8 +1,10 @@
import APIClient from '../../functions/APIClient';
import { VideoResponseType } from '../../pages/Video';
import { VideoType } from '../../pages/Home';
const loadVideoById = async (youtubeId: string): Promise<VideoResponseType> => {
return APIClient(`/api/video/${youtubeId}/`);
export type VideoResponseType = VideoType;
const loadVideoById = async (youtubeId: string) => {
return APIClient<VideoResponseType>(`/api/video/${youtubeId}/`);
};
export default loadVideoById;

View File

@@ -21,9 +21,7 @@ type FilterType = {
type?: VideoTypes;
};
const loadVideoListByFilter = async (
filter: FilterType,
): Promise<VideoListByFilterResponseType> => {
const loadVideoListByFilter = async (filter: FilterType) => {
const searchParams = new URLSearchParams();
if (filter.playlist) {
@@ -39,7 +37,7 @@ const loadVideoListByFilter = async (
if (filter.type) searchParams.append('type', filter.type);
const endpoint = `/api/video/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
return APIClient(endpoint);
return APIClient<VideoListByFilterResponseType>(endpoint);
};
export default loadVideoListByFilter;

View File

@@ -25,8 +25,8 @@ export type VideoNavResponseType = {
};
};
const loadVideoNav = async (youtubeVideoId: string): Promise<VideoNavResponseType[]> => {
return APIClient(`/api/video/${youtubeVideoId}/nav/`);
const loadVideoNav = async (youtubeVideoId: string) => {
return APIClient<VideoNavResponseType[]>(`/api/video/${youtubeVideoId}/nav/`);
};
export default loadVideoNav;

View File

@@ -1,7 +1,9 @@
import { Fragment } from 'react';
import StatsInfoBoxItem from './StatsInfoBoxItem';
import formatNumbers from '../functions/formatNumbers';
import { ChannelStatsType, PlaylistStatsType, DownloadStatsType } from '../pages/SettingsDashboard';
import { ChannelStatsType } from '../api/loader/loadStatsChannel';
import { PlaylistStatsType } from '../api/loader/loadStatsPlaylist';
import { DownloadStatsType } from '../api/loader/loadStatsDownload';
type ApplicationStatsProps = {
channelStats?: ChannelStatsType;

View File

@@ -2,7 +2,7 @@ import humanFileSize from '../functions/humanFileSize';
import formatNumbers from '../functions/formatNumbers';
import { Link } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList';
import { BiggestChannelsStatsType } from '../pages/SettingsDashboard';
import { BiggestChannelsStatsType } from '../api/loader/loadStatsBiggestChannels';
type BiggestChannelsStatsProps = {
biggestChannelsStatsByCount?: BiggestChannelsStatsType;

View File

@@ -1,7 +1,7 @@
import humanFileSize from '../functions/humanFileSize';
import formatDate from '../functions/formatDates';
import formatNumbers from '../functions/formatNumbers';
import { DownloadHistoryStatsType } from '../pages/SettingsDashboard';
import { DownloadHistoryStatsType } from '../api/loader/loadStatsDownloadHistory';
type DownloadHistoryStatsProps = {
downloadHistoryStats?: DownloadHistoryStatsType;

View File

@@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { VideoResponseType } from '../pages/Video';
import VideoPlayer from './VideoPlayer';
import loadVideoById from '../api/loader/loadVideoById';
import loadVideoById, { VideoResponseType } from '../api/loader/loadVideoById';
import iconClose from '/img/icon-close.svg';
import iconEye from '/img/icon-eye.svg';
import iconThumb from '/img/icon-thumb.svg';
@@ -13,6 +12,7 @@ import { Link, useSearchParams } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList';
import loadPlaylistById from '../api/loader/loadPlaylistById';
import { useAppSettingsStore } from '../stores/AppSettingsStore';
import { ApiResponseType } from '../functions/APIClient';
type Playlist = {
id: string;
@@ -32,9 +32,11 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
const [refresh, setRefresh] = useState(true);
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
const [playlists, setPlaylists] = useState<PlaylistList>();
const { data: videoResponseData } = videoResponse ?? {};
useEffect(() => {
(async () => {
if (!videoId) {
@@ -43,27 +45,31 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
inlinePlayerRef.current?.scrollIntoView();
if (refresh || videoId !== videoResponse?.youtube_id) {
if (refresh || videoId !== videoResponseData?.youtube_id) {
const videoResponse = await loadVideoById(videoId);
const playlistIds = videoResponse.playlist;
const { data: videoResponseData } = videoResponse ?? {};
const playlistIds = videoResponseData?.playlist || [];
if (playlistIds !== undefined) {
const playlists = await Promise.all(
playlistIds.map(async playlistid => {
const playlistResponse = await loadPlaylistById(playlistid);
return playlistResponse;
const { data: playlistResponseData } = playlistResponse ?? {};
return playlistResponseData;
}),
);
const playlistsFiltered = playlists
.filter(playlist => {
return playlist.playlist_subscribed;
return playlist?.playlist_subscribed;
})
.map(playlist => {
return {
id: playlist.playlist_id,
name: playlist.playlist_name,
id: playlist?.playlist_id || '',
name: playlist?.playlist_name || '',
};
});
@@ -77,11 +83,11 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoId, refresh]);
if (videoResponse === undefined || videoId === null) {
if (videoResponseData === undefined || videoId === null) {
return <div ref={inlinePlayerRef} className="player-wrapper" />;
}
const video = videoResponse;
const video = videoResponseData;
const name = video.title;
const channelId = video.channel.channel_id;
const channelName = video.channel.channel_name;
@@ -99,7 +105,7 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
<div ref={inlinePlayerRef} className="player-wrapper">
<div className="video-player">
<VideoPlayer
video={videoResponse}
video={videoResponseData}
sponsorBlock={sponsorblock}
embed={true}
autoplay={true}

View File

@@ -22,7 +22,11 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
const updatedUserConfig = await updateUserConfig(config);
setUserConfig(updatedUserConfig);
const { data: updatedUserConfigData } = updatedUserConfig;
if (updatedUserConfigData) {
setUserConfig(updatedUserConfigData);
}
};
return (

View File

@@ -46,7 +46,12 @@ async function castVideoProgress(
currentProgress: currentTime,
});
if (videoProgressResponse.watched && video.player.watched !== videoProgressResponse.watched) {
const { data: videoProgressResponseData } = videoProgressResponse ?? {};
if (
videoProgressResponseData?.watched &&
video.player.watched !== videoProgressResponseData.watched
) {
onWatchStateChanged?.(true);
}
}

View File

@@ -1,20 +1,11 @@
import { Fragment, useEffect, useState } from 'react';
import loadNotifications, { NotificationPages } from '../api/loader/loadNotifications';
import loadNotifications, {
NotificationPages,
NotificationResponseType,
} 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[];
import { ApiResponseType } from '../functions/APIClient';
type NotificationsProps = {
pageName: NotificationPages;
@@ -29,13 +20,17 @@ const Notifications = ({
update,
setShouldRefresh,
}: NotificationsProps) => {
const [notificationResponse, setNotificationResponse] = useState<NotificationResponseType>([]);
const [notificationResponse, setNotificationResponse] =
useState<ApiResponseType<NotificationResponseType>>();
const { data: notificationResponseData } = notificationResponse ?? {};
useEffect(() => {
const intervalId = setInterval(async () => {
const notifications = await loadNotifications(pageName, includeReindex);
const { data: notificationsData } = notifications ?? {};
if (notifications.length === 0) {
if (notificationsData?.length === 0) {
setNotificationResponse(notifications);
clearInterval(intervalId);
setShouldRefresh?.(true);
@@ -52,13 +47,13 @@ const Notifications = ({
};
}, [pageName, update, setShouldRefresh, includeReindex]);
if (notificationResponse.length === 0) {
if (notificationResponseData?.length === 0) {
return [];
}
return (
<>
{notificationResponse.map(notification => (
{notificationResponseData?.map(notification => (
<div
id={notification.id}
className={`notification ${notification.level}`}

View File

@@ -2,7 +2,7 @@ import { Fragment } from 'react';
import humanFileSize from '../functions/humanFileSize';
import StatsInfoBoxItem from './StatsInfoBoxItem';
import formatNumbers from '../functions/formatNumbers';
import { VideoStatsType } from '../pages/SettingsDashboard';
import { VideoStatsType } from '../api/loader/loadStatsVideo';
type OverviewStatsProps = {
videoStats?: VideoStatsType;

View File

@@ -1,5 +1,5 @@
import updateVideoProgressById from '../api/actions/updateVideoProgressById';
import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video';
import { SponsorBlockSegmentType, SponsorBlockType } from '../pages/Video';
import {
Dispatch,
Fragment,
@@ -13,6 +13,7 @@ import formatTime from '../functions/formatTime';
import { useSearchParams } from 'react-router-dom';
import getApiUrl from '../configuration/getApiUrl';
import { useKeyPress } from '../functions/useKeypressHook';
import { VideoResponseType } from '../api/loader/loadVideoById';
const VIDEO_PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3];
@@ -95,7 +96,9 @@ const handleTimeUpdate =
currentProgress: currentTime,
});
if (videoProgressResponse.watched && watched !== videoProgressResponse.watched) {
const { data: videoProgressResponseData } = videoProgressResponse ?? {};
if (videoProgressResponseData?.watched && watched !== videoProgressResponseData.watched) {
onWatchStateChanged?.(true);
}
}
@@ -185,7 +188,9 @@ const VideoPlayer = ({
currentProgress: currentTime,
});
if (videoProgressResponse.watched && watched !== videoProgressResponse.watched) {
const { data: videoProgressResponseData } = videoProgressResponse;
if (videoProgressResponseData?.watched && watched !== videoProgressResponseData.watched) {
onWatchStateChanged?.(true);
}

View File

@@ -2,7 +2,7 @@ import { Fragment } from 'react';
import humanFileSize from '../functions/humanFileSize';
import StatsInfoBoxItem from './StatsInfoBoxItem';
import formatNumbers from '../functions/formatNumbers';
import { VideoStatsType } from '../pages/SettingsDashboard';
import { VideoStatsType } from '../api/loader/loadStatsVideo';
type VideoTypeStatsProps = {
videoStats?: VideoStatsType;

View File

@@ -1,7 +1,7 @@
import { Fragment } from 'react';
import StatsInfoBoxItem from './StatsInfoBoxItem';
import formatNumbers from '../functions/formatNumbers';
import { WatchProgressStatsType } from '../pages/SettingsDashboard';
import { WatchProgressStatsType } from '../api/loader/loadStatsWatchProgress';
const formatProgress = (progress: number) => {
return (Number(progress) * 100).toFixed(2) ?? '0';

View File

@@ -15,10 +15,20 @@ export interface ApiError {
message: string;
}
const APIClient = async (
export type ResponseErrorType = {
error: string;
};
export type ApiResponseType<T> = {
data?: T;
error?: ResponseErrorType;
status: number;
};
const APIClient = async <T>(
endpoint: string,
{ method = 'GET', body, headers = {}, ...options }: ApiClientOptions = {},
) => {
): Promise<ApiResponseType<T>> => {
const apiUrl = getApiUrl();
const csrfToken = getCookie('csrftoken');
@@ -55,34 +65,37 @@ const APIClient = async (
throw new Error('Forbidden: Access denied.');
}
if (response.status === 404) {
throw {
status: 404,
message: 'Resource not found',
} as ApiError;
}
let data;
// expected empty response
if (response.status === 204) {
data = null;
return data;
return {
data: undefined,
error: undefined,
status: response.status,
};
}
// Try parsing response data
try {
data = await response.json();
const responseJson = await response.json();
const hasErrorMessage = responseJson.error;
return {
data: !hasErrorMessage ? responseJson : undefined,
error: hasErrorMessage ? responseJson : undefined,
status: response.status,
};
} catch (error) {
data = null;
console.error(`error fetching data: ${error}`);
}
if (!response.ok) {
throw new Error(data?.detail || 'An error occurred while processing the request.');
return {
data: undefined,
error: {
error: `error fetching data: ${error}`,
},
status: response.status,
};
}
return data;
};
export default APIClient;

View File

@@ -27,7 +27,7 @@ import ChannelAbout from './pages/ChannelAbout';
import Download from './pages/Download';
import loadUserAccount from './api/loader/loadUserAccount';
import loadAppsettingsConfig from './api/loader/loadAppsettingsConfig';
import NotFound from './pages/404Page';
import NotFound from './pages/NotFound';
const router = createBrowserRouter(
[
@@ -51,9 +51,9 @@ const router = createBrowserRouter(
return redirect(Routes.Login);
}
const userConfig = await loadUserMeConfig();
const userAccount = await loadUserAccount();
const appSettings = await loadAppsettingsConfig();
const { data: userConfig } = await loadUserMeConfig();
const { data: userAccount } = await loadUserAccount();
const { data: appSettings } = await loadAppsettingsConfig();
return { userConfig, userAccount, appSettings, auth: authData };
},
@@ -149,6 +149,7 @@ const router = createBrowserRouter(
{
path: '*',
element: <NotFound />,
errorElement: <ErrorPage />,
},
],
{ basename: import.meta.env.BASE_URL },

View File

@@ -1,8 +1,7 @@
import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
import ChannelOverview from '../components/ChannelOverview';
import { useEffect, useState } from 'react';
import loadChannelById from '../api/loader/loadChannelById';
import { ChannelResponseType } from './ChannelBase';
import loadChannelById, { ChannelResponseType } from '../api/loader/loadChannelById';
import Linkify from '../components/Linkify';
import deleteChannel from '../api/actions/deleteChannel';
import Routes from '../configuration/routes/RouteList';
@@ -16,6 +15,7 @@ import useIsAdmin from '../functions/useIsAdmin';
import InputConfig from '../components/InputConfig';
import ToggleConfig from '../components/ToggleConfig';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiResponseType } from '../functions/APIClient';
export type ChannelBaseOutletContextType = {
currentPage: number;
@@ -45,7 +45,7 @@ const ChannelAbout = () => {
const [reindex, setReindex] = useState(false);
const [refresh, setRefresh] = useState(true);
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
const [downloadFormat, setDownloadFormat] = useState<string | null>(null);
const [autoDeleteAfter, setAutoDeleteAfter] = useState<number | null>(null);
@@ -55,24 +55,31 @@ const ChannelAbout = () => {
const [pageSizeShorts, setPageSizeShorts] = useState<number | null>(null);
const [pageSizeStreams, setPageSizeStreams] = useState<number | null>(null);
const channel = channelResponse;
const { data: channelResponseData } = channelResponse ?? {};
const channel = channelResponseData;
useEffect(() => {
(async () => {
if (refresh) {
const channelResponse = await loadChannelById(channelId);
const { data: channelResponseData } = channelResponse;
setChannelResponse(channelResponse);
setDownloadFormat(channelResponse?.channel_overwrites?.download_format ?? null);
setAutoDeleteAfter(channelResponse?.channel_overwrites?.autodelete_days ?? null);
setIndexPlaylists(channelResponse?.channel_overwrites?.index_playlists ?? false);
setEnableSponsorblock(channelResponse?.channel_overwrites?.integrate_sponsorblock ?? null);
setPageSizeVideo(channelResponse?.channel_overwrites?.subscriptions_channel_size ?? null);
setDownloadFormat(channelResponseData?.channel_overwrites?.download_format ?? null);
setAutoDeleteAfter(channelResponseData?.channel_overwrites?.autodelete_days ?? null);
setIndexPlaylists(channelResponseData?.channel_overwrites?.index_playlists ?? false);
setEnableSponsorblock(
channelResponseData?.channel_overwrites?.integrate_sponsorblock ?? null,
);
setPageSizeVideo(
channelResponseData?.channel_overwrites?.subscriptions_channel_size ?? null,
);
setPageSizeShorts(
channelResponse?.channel_overwrites?.subscriptions_shorts_channel_size ?? null,
channelResponseData?.channel_overwrites?.subscriptions_shorts_channel_size ?? null,
);
setPageSizeStreams(
channelResponse?.channel_overwrites?.subscriptions_live_channel_size ?? null,
channelResponseData?.channel_overwrites?.subscriptions_live_channel_size ?? null,
);
setRefresh(false);

View File

@@ -1,54 +1,49 @@
import { Link, Outlet, useOutletContext, useParams } from 'react-router-dom';
import Routes from '../configuration/routes/RouteList';
import { ChannelType } from './Channels';
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 loadChannelById, { ChannelResponseType } from '../api/loader/loadChannelById';
import useIsAdmin from '../functions/useIsAdmin';
import { ApiError } from '../functions/APIClient';
import NotFound from './404Page';
import { ApiResponseType } from '../functions/APIClient';
import NotFound from './NotFound';
type ChannelParams = {
channelId: string;
};
export type ChannelResponseType = ChannelType;
const ChannelBase = () => {
const { channelId } = useParams() as ChannelParams;
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const [error, setError] = useState<ApiError | null>(null);
const isAdmin = useIsAdmin();
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [channelNav, setChannelNav] = useState<ChannelNavResponseType>();
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
const [channelNav, setChannelNav] = useState<ApiResponseType<ChannelNavResponseType>>();
const [startNotification, setStartNotification] = useState(false);
const channel = channelResponse;
const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
const { data: channelResponseData, error: channelResponseError } = channelResponse ?? {};
const { data: channelNavData } = channelNav ?? {};
const channel = channelResponseData;
const { has_streams, has_shorts, has_playlists, has_pending } = channelNavData || {};
useEffect(() => {
(async () => {
setError(null);
try {
const channelResponse = await loadChannelById(channelId);
setChannelResponse(channelResponse);
} catch (err) {
if ((err as ApiError).status === 404) {
setError(err as ApiError);
} else {
console.error('Failed to fetch item:', err);
}
}
const channelResponse = await loadChannelById(channelId);
setChannelResponse(channelResponse);
const channelNavResponse = await loadChannelNav(channelId);
setChannelNav(channelNavResponse);
})();
}, [channelId]);
if (error) return <NotFound failType="channel" />;
const errorMessage = channelResponseError?.error;
if (errorMessage) {
return <NotFound failType="channel" />;
}
if (!channelId) {
return [];

View File

@@ -5,12 +5,12 @@ 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 loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList';
import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg';
import { useUserConfigStore } from '../stores/UserConfigStore';
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
import { ApiResponseType } from '../functions/APIClient';
const ChannelPlaylist = () => {
const { channelId } = useParams();
@@ -19,17 +19,24 @@ const ChannelPlaylist = () => {
const [refreshPlaylists, setRefreshPlaylists] = useState(false);
const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>();
const [playlistsResponse, setPlaylistsResponse] =
useState<ApiResponseType<PlaylistsResponseType>>();
const playlistList = playlistsResponse?.data;
const pagination = playlistsResponse?.paginate;
const { data: playlistsResponseData } = playlistsResponse ?? {};
const playlistList = playlistsResponseData?.data;
const pagination = playlistsResponseData?.paginate;
const view = userConfig.view_style_playlist;
const showSubedOnly = userConfig.show_subed_only;
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
const updatedUserConfig = await updateUserConfig(config);
setUserConfig(updatedUserConfig);
const { data: updatedUserConfigData } = updatedUserConfig;
if (updatedUserConfigData) {
setUserConfig(updatedUserConfigData);
}
};
useEffect(() => {

View File

@@ -7,8 +7,7 @@ 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 loadChannelById, { ChannelResponseType } from '../api/loader/loadChannelById';
import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import updateWatchedState from '../api/actions/updateWatchedState';
@@ -21,6 +20,7 @@ import loadChannelAggs, { ChannelAggsType } from '../api/loader/loadChannelAggs'
import humanFileSize from '../functions/humanFileSize';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { FileSizeUnits } from '../api/actions/updateUserConfig';
import { ApiResponseType } from '../functions/APIClient';
type ChannelParams = {
channelId: string;
@@ -39,15 +39,21 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
const [refresh, setRefresh] = useState(false);
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
const [videoAggsResponse, setVideoAggsResponse] = useState<ChannelAggsType>();
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
const [videoResponse, setVideoReponse] =
useState<ApiResponseType<VideoListByFilterResponseType>>();
const [videoAggsResponse, setVideoAggsResponse] = useState<ApiResponseType<ChannelAggsType>>();
const channel = channelResponse;
const videoList = videoResponse?.data;
const pagination = videoResponse?.paginate;
const { data: channelResponseData } = channelResponse ?? {};
const { data: videoResponseData } = videoResponse ?? {};
const { data: videoAggsResponseData } = videoAggsResponse ?? {};
const hasVideos = videoResponse?.data?.length !== 0;
const channel = channelResponseData;
const videoList = videoResponseData?.data;
const pagination = videoResponseData?.paginate;
const videoAggs = videoAggsResponseData;
const hasVideos = videoResponseData?.data?.length !== 0;
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
const view = userConfig.view_style_home;
@@ -109,14 +115,13 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
setRefresh={setRefresh}
/>
<div className="info-box-item">
{videoAggsResponse && (
{videoAggs && (
<>
<p>
{videoAggsResponse.total_items.value} videos{' '}
<span className="space-carrot">|</span>{' '}
{videoAggsResponse.total_duration.value_str} playback{' '}
{videoAggs.total_items.value} videos <span className="space-carrot">|</span>{' '}
{videoAggs.total_duration.value_str} playback{' '}
<span className="space-carrot">|</span> Total size{' '}
{humanFileSize(videoAggsResponse.total_size.value, useSiUnits)}
{humanFileSize(videoAggs.total_size.value, useSiUnits)}
</p>
<div className="button-box">
<Button

View File

@@ -1,11 +1,10 @@
import { useOutletContext } from 'react-router-dom';
import loadChannelList from '../api/loader/loadChannelList';
import loadChannelList, { ChannelsListResponse } 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 Pagination from '../components/Pagination';
import { OutletContextType } from './Base';
import ChannelList from '../components/ChannelList';
import ScrollToTopOnNavigate from '../components/ScrollToTop';
@@ -15,6 +14,7 @@ import updateBulkChannelSubscriptions from '../api/actions/updateBulkChannelSubs
import useIsAdmin from '../functions/useIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
import { ApiResponseType } from '../functions/APIClient';
type ChannelOverwritesType = {
download_format: string | null;
@@ -42,31 +42,31 @@ export type ChannelType = {
channel_views: number;
};
type ChannelsListResponse = {
data: ChannelType[];
paginate: PaginationType;
config?: ConfigType;
};
const Channels = () => {
const { userConfig, setUserConfig } = useUserConfigStore();
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
const isAdmin = useIsAdmin();
const [channelListResponse, setChannelListResponse] = useState<ChannelsListResponse>();
const [channelListResponse, setChannelListResponse] =
useState<ApiResponseType<ChannelsListResponse>>();
const [showAddForm, setShowAddForm] = useState(false);
const [refresh, setRefresh] = useState(true);
const [showNotification, setShowNotification] = useState(false);
const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
const channels = channelListResponse?.data;
const pagination = channelListResponse?.paginate;
// const channelCount = pagination?.total_hits;
const { data: channelListResponseData } = channelListResponse ?? {};
const channels = channelListResponseData?.data;
const pagination = channelListResponseData?.paginate;
const hasChannels = channels?.length !== 0;
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
const updatedUserConfig = await updateUserConfig(config);
setUserConfig(updatedUserConfig);
const { data: updatedUserConfigData } = updatedUserConfig ;
if (updatedUserConfigData) {
setUserConfig(updatedUserConfigData);
}
};
useEffect(() => {
@@ -187,7 +187,6 @@ const Channels = () => {
/>
</div>
</div>
{/* {hasChannels && <h2>Total channels: {channelCount}</h2>} */}
<div className={`channel-list ${userConfig.view_style_channel}`}>
{!hasChannels && <h2>No channels found...</h2>}

View File

@@ -20,6 +20,7 @@ import DownloadListItem from '../components/DownloadListItem';
import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs';
import { useUserConfigStore } from '../stores/UserConfigStore';
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
import { ApiResponseType } from '../functions/APIClient';
type Download = {
auto_start: boolean;
@@ -61,18 +62,22 @@ const Download = () => {
const [downloadQueueText, setDownloadQueueText] = useState('');
const [downloadResponse, setDownloadResponse] = useState<DownloadResponseType>();
const [downloadAggsResponse, setDownloadAggsResponse] = useState<DownloadAggsType>();
const [downloadResponse, setDownloadResponse] = useState<ApiResponseType<DownloadResponseType>>();
const [downloadAggsResponse, setDownloadAggsResponse] =
useState<ApiResponseType<DownloadAggsType>>();
const downloadList = downloadResponse?.data;
const pagination = downloadResponse?.paginate;
const channelAggsList = downloadAggsResponse?.buckets;
const { data: downloadResponseData } = downloadResponse ?? {};
const { data: downloadAggsResponseData } = downloadAggsResponse ?? {};
const downloadList = downloadResponseData?.data;
const pagination = downloadResponseData?.paginate;
const channelAggsList = downloadAggsResponseData?.buckets;
const downloadCount = pagination?.total_hits;
const channel_filter_name =
downloadResponse?.data?.length && downloadResponse?.data?.length > 0
? downloadResponse?.data[0].channel_name
downloadResponseData?.data?.length && downloadResponseData?.data?.length > 0
? downloadResponseData?.data[0].channel_name
: '';
const view = userConfig.view_style_downloads;
@@ -84,19 +89,28 @@ const Download = () => {
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
const updatedUserConfig = await updateUserConfig(config);
setUserConfig(updatedUserConfig);
const { data: updatedUserConfigData } = updatedUserConfig ;
if (updatedUserConfigData) {
setUserConfig(updatedUserConfigData);
}
};
useEffect(() => {
(async () => {
const videos = await loadDownloadQueue(currentPage, channelFilterFromUrl, showIgnored);
const videoCount = videos?.paginate?.total_hits;
const videosResponse = await loadDownloadQueue(
currentPage,
channelFilterFromUrl,
showIgnored,
);
const { data: channelResponseData } = videosResponse ?? {};
const videoCount = channelResponseData?.paginate?.total_hits;
if (videoCount && lastVideoCount !== videoCount) {
setLastVideoCount(videoCount);
}
setDownloadResponse(videos);
setDownloadResponse(videosResponse);
setRefresh(false);
})();

View File

@@ -14,6 +14,7 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import { SponsorBlockType } from './Video';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiResponseType } from '../functions/APIClient';
export type PlayerType = {
watched: boolean;
@@ -110,15 +111,19 @@ const Home = () => {
const [refreshVideoList, setRefreshVideoList] = useState(false);
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
const [videoResponse, setVideoReponse] =
useState<ApiResponseType<VideoListByFilterResponseType>>();
const [continueVideoResponse, setContinueVideoResponse] =
useState<VideoListByFilterResponseType>();
useState<ApiResponseType<VideoListByFilterResponseType>>();
const videoList = videoResponse?.data;
const pagination = videoResponse?.paginate;
const continueVideos = continueVideoResponse?.data;
const { data: videoResponseData } = videoResponse ?? {};
const { data: continueVideoResponseData } = continueVideoResponse ?? {};
const hasVideos = videoResponse?.data?.length !== 0;
const videoList = videoResponseData?.data;
const pagination = videoResponseData?.paginate;
const continueVideos = continueVideoResponseData?.data;
const hasVideos = videoResponseData?.data?.length !== 0;
const isGridView = userConfig.view_style_home === ViewStyles.grid;
const gridView = isGridView ? `boxed-${userConfig.grid_items}` : '';

View File

@@ -4,6 +4,7 @@ import Routes from '../configuration/routes/RouteList';
const NotFound = ({ failType = 'page' }) => {
useColours();
return (
<>
<title>404 | Not found</title>

View File

@@ -4,7 +4,7 @@ import loadPlaylistById, { PlaylistResponseType } from '../api/loader/loadPlayli
import { OutletContextType } from './Base';
import { VideoType } from './Home';
import Filterbar from '../components/Filterbar';
import loadChannelById from '../api/loader/loadChannelById';
import loadChannelById, { ChannelResponseType } from '../api/loader/loadChannelById';
import VideoList from '../components/VideoList';
import Pagination, { PaginationType } from '../components/Pagination';
import ChannelOverview from '../components/ChannelOverview';
@@ -13,7 +13,6 @@ 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';
@@ -23,8 +22,8 @@ import Button from '../components/Button';
import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
import useIsAdmin from '../functions/useIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiError } from '../functions/APIClient';
import NotFound from './404Page';
import { ApiResponseType } from '../functions/APIClient';
import NotFound from './NotFound';
export type VideoResponseType = {
data?: VideoType[];
@@ -33,7 +32,6 @@ export type VideoResponseType = {
const Playlist = () => {
const { playlistId } = useParams();
const [error, setError] = useState<ApiError | null>(null);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const videoId = searchParams.get('videoId');
@@ -47,16 +45,20 @@ const Playlist = () => {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [reindex, setReindex] = useState(false);
const [playlistResponse, setPlaylistResponse] = useState<PlaylistResponseType>();
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
const [playlistResponse, setPlaylistResponse] = useState<ApiResponseType<PlaylistResponseType>>();
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
const playlist = playlistResponse;
const channel = channelResponse;
const videos = videoResponse?.data;
const pagination = videoResponse?.paginate;
const { data: playlistResponseData, error: playlistResponseError } = playlistResponse ?? {};
const { data: channelResponseData } = channelResponse ?? {};
const { data: videoResponseData } = videoResponse ?? {};
const palylistEntries = playlistResponse?.playlist_entries;
const playlist = playlistResponseData;
const channel = channelResponseData;
const videos = videoResponseData?.data;
const pagination = videoResponseData?.paginate;
const palylistEntries = playlistResponseData?.playlist_entries;
const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
const videoInPlaylistCount = pagination?.total_hits;
@@ -69,27 +71,22 @@ const Playlist = () => {
useEffect(() => {
(async () => {
setError(null);
try {
const playlist = await loadPlaylistById(playlistId);
const video = await loadVideoListByFilter({
playlist: playlistId,
page: currentPage,
watch: hideWatched ? 'unwatched' : undefined,
});
setPlaylistResponse(playlist);
setVideoResponse(video);
const isCustomPlaylist = playlist?.playlist_type === 'custom';
if (!isCustomPlaylist) {
const channel = await loadChannelById(playlist.playlist_channel_id);
setChannelResponse(channel);
}
} catch (err) {
if ((err as ApiError).status === 404) {
setError(err as ApiError);
} else {
console.error('Failed to fetch item:', err);
}
const playlist = await loadPlaylistById(playlistId);
const video = await loadVideoListByFilter({
playlist: playlistId,
page: currentPage,
watch: hideWatched ? 'unwatched' : undefined,
});
setPlaylistResponse(playlist);
setVideoResponse(video);
const { data: playlistResponseData } = playlist ?? {};
const isCustomPlaylist = playlistResponseData?.playlist_type === 'custom';
if (!isCustomPlaylist) {
const channel = await loadChannelById(playlistResponseData?.playlist_channel_id || '');
setChannelResponse(channel);
}
setRefresh(false);
})();
@@ -103,7 +100,11 @@ const Playlist = () => {
videoId,
]);
if (error) return <NotFound failType="playlist" />;
const errorMessage = playlistResponseError?.error;
if (errorMessage) {
return <NotFound failType="playlist" />;
}
if (!playlist || !playlistId) return [];

View File

@@ -6,8 +6,8 @@ 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 Pagination, { PaginationType } from '../components/Pagination';
import loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList';
import Pagination from '../components/Pagination';
import PlaylistList from '../components/PlaylistList';
import updateBulkPlaylistSubscriptions from '../api/actions/updateBulkPlaylistSubscriptions';
import createCustomPlaylist from '../api/actions/createCustomPlaylist';
@@ -17,12 +17,7 @@ import useIsAdmin from '../functions/useIsAdmin';
import { useUserConfigStore } from '../stores/UserConfigStore';
import Notifications from '../components/Notifications';
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
import { PlaylistType } from '../api/loader/loadPlaylistById';
export type PlaylistsResponseType = {
data?: PlaylistType[];
paginate?: PaginationType;
};
import { ApiResponseType } from '../functions/APIClient';
const Playlists = () => {
const { userConfig, setUserConfig } = useUserConfigStore();
@@ -35,12 +30,14 @@ const Playlists = () => {
const [playlistsToAddText, setPlaylistsToAddText] = useState('');
const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
const [playlistResponse, setPlaylistReponse] = useState<PlaylistsResponseType>();
const [playlistResponse, setPlaylistReponse] = useState<ApiResponseType<PlaylistsResponseType>>();
const playlistList = playlistResponse?.data;
const pagination = playlistResponse?.paginate;
const { data: playlistResponseData } = playlistResponse ?? {};
const hasPlaylists = playlistResponse?.data?.length !== 0;
const playlistList = playlistResponseData?.data;
const pagination = playlistResponseData?.paginate;
const hasPlaylists = playlistResponseData?.data?.length !== 0;
const view = userConfig.view_style_playlist;
const showSubedOnly = userConfig.show_subed_only;
@@ -61,7 +58,11 @@ const Playlists = () => {
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
const updatedUserConfig = await updateUserConfig(config);
setUserConfig(updatedUserConfig);
const { data: updatedUserConfigData } = updatedUserConfig;
if (updatedUserConfigData) {
setUserConfig(updatedUserConfigData);
}
};
return (

View File

@@ -1,8 +1,6 @@
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { VideoType } from './Home';
import loadSearch from '../api/loader/loadSearch';
import { ChannelType } from './Channels';
import loadSearch, { SearchResultsType } from '../api/loader/loadSearch';
import VideoList from '../components/VideoList';
import ChannelList from '../components/ChannelList';
import PlaylistList from '../components/PlaylistList';
@@ -11,28 +9,20 @@ import { ViewStyles } from '../configuration/constants/ViewStyle';
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
import SearchExampleQueries from '../components/SearchExampleQueries';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { PlaylistType } from '../api/loader/loadPlaylistById';
import { ApiResponseType } from '../functions/APIClient';
const EmptySearchResponse: SearchResultsType = {
results: {
video_results: [],
channel_results: [],
playlist_results: [],
fulltext_results: [],
const EmptySearchResponse: ApiResponseType<SearchResultsType> = {
data: {
results: {
video_results: [],
channel_results: [],
playlist_results: [],
fulltext_results: [],
},
queryType: 'simple',
},
queryType: 'simple',
};
type SearchResultType = {
video_results: VideoType[];
channel_results: ChannelType[];
playlist_results: PlaylistType[];
fulltext_results: [];
};
type SearchResultsType = {
results: SearchResultType;
queryType: string;
error: undefined,
status: 200,
};
const Search = () => {
@@ -47,15 +37,17 @@ const Search = () => {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState<string>('');
const [searchResults, setSearchResults] = useState<SearchResultsType>();
const [searchResults, setSearchResults] = useState<ApiResponseType<SearchResultsType>>();
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 { data: searchResultsData } = searchResults ?? {};
const videoList = searchResultsData?.results.video_results;
const channelList = searchResultsData?.results.channel_results;
const playlistList = searchResultsData?.results.playlist_results;
const fulltextList = searchResultsData?.results.fulltext_results;
const queryType = searchResultsData?.queryType;
const hasSearchQuery = searchTerm.length > 0;
const hasVideos = Number(videoList?.length) > 0;

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import loadBackupList from '../api/loader/loadBackupList';
import loadBackupList, { BackupListType } from '../api/loader/loadBackupList';
import SettingsNavigation from '../components/SettingsNavigation';
import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
import updateTaskByName from '../api/actions/updateTaskByName';
@@ -7,16 +7,7 @@ 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[];
import { ApiResponseType } from '../functions/APIClient';
const SettingsActions = () => {
const [deleteIgnored, setDeleteIgnored] = useState(false);
@@ -27,9 +18,11 @@ const SettingsActions = () => {
const [isRestoringBackup, setIsRestoringBackup] = useState(false);
const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
const [backupListResponse, setBackupListResponse] = useState<BackupListType>();
const [backupListResponse, setBackupListResponse] = useState<ApiResponseType<BackupListType>>();
const backups = backupListResponse;
const { data: backupListResponseData } = backupListResponse ?? {};
const backups = backupListResponseData;
const hasBackups = !!backups && backups?.length > 0;
useEffect(() => {

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import loadSnapshots from '../api/loader/loadSnapshots';
import loadSnapshots, { SnapshotListType } from '../api/loader/loadSnapshots';
import Notifications from '../components/Notifications';
import PaginationDummy from '../components/PaginationDummy';
import SettingsNavigation from '../components/SettingsNavigation';
@@ -21,28 +21,11 @@ import deletePoToken from '../api/actions/deletePoToken';
import updatePoToken from '../api/actions/updatePoToken';
import { useUserConfigStore } from '../stores/UserConfigStore';
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;
cookieState: CookieStateType;
cookieState?: CookieStateType;
};
const SettingsApplication = () => {
@@ -102,50 +85,55 @@ const SettingsApplication = () => {
const fetchData = async () => {
const snapshotResponse = await loadSnapshots();
const appSettingsConfig = await loadAppsettingsConfig();
const apiToken = await loadApiToken();
const cookieState = await loadCookie();
const apiTokenResponse = await loadApiToken();
const cookieStateResponse = await loadCookie();
const { data: snapshotResponseData } = snapshotResponse ?? {};
const { data: appSettingsConfigData } = appSettingsConfig ?? {};
const { data: apiTokenResponseData } = apiTokenResponse ?? {};
const { data: cookieStateResponseData } = cookieStateResponse ?? {};
// Subscriptions
setVideoPageSize(appSettingsConfig.subscriptions.channel_size);
setLivePageSize(appSettingsConfig.subscriptions.live_channel_size);
setShortPageSize(appSettingsConfig.subscriptions.shorts_channel_size);
setIsAutostart(appSettingsConfig.subscriptions.auto_start);
setVideoPageSize(appSettingsConfigData?.subscriptions.channel_size || null);
setLivePageSize(appSettingsConfigData?.subscriptions.live_channel_size || null);
setShortPageSize(appSettingsConfigData?.subscriptions.shorts_channel_size || null);
setIsAutostart(appSettingsConfigData?.subscriptions.auto_start || false);
// Downloads
setCurrentDownloadSpeed(appSettingsConfig.downloads.limit_speed);
setCurrentThrottledRate(appSettingsConfig.downloads.throttledratelimit);
setCurrentScrapingSleep(appSettingsConfig.downloads.sleep_interval);
setCurrentAutodelete(appSettingsConfig.downloads.autodelete_days);
setCurrentDownloadSpeed(appSettingsConfigData?.downloads.limit_speed || null);
setCurrentThrottledRate(appSettingsConfigData?.downloads.throttledratelimit || null);
setCurrentScrapingSleep(appSettingsConfigData?.downloads.sleep_interval || null);
setCurrentAutodelete(appSettingsConfigData?.downloads.autodelete_days || null);
// Download Format
setDownloadsFormat(appSettingsConfig.downloads.format);
setDownloadsFormatSort(appSettingsConfig.downloads.format_sort);
setDownloadsExtractorLang(appSettingsConfig.downloads.extractor_lang);
setEmbedMetadata(appSettingsConfig.downloads.add_metadata);
setEmbedThumbnail(appSettingsConfig.downloads.add_thumbnail);
setDownloadsFormat(appSettingsConfigData?.downloads.format || null);
setDownloadsFormatSort(appSettingsConfigData?.downloads.format_sort || null);
setDownloadsExtractorLang(appSettingsConfigData?.downloads.extractor_lang || null);
setEmbedMetadata(appSettingsConfigData?.downloads.add_metadata || false);
setEmbedThumbnail(appSettingsConfigData?.downloads.add_thumbnail || false);
// Subtitles
setSubtitleLang(appSettingsConfig.downloads.subtitle);
setSubtitleSource(appSettingsConfig.downloads.subtitle_source);
setIndexSubtitles(appSettingsConfig.downloads.subtitle_index);
setSubtitleLang(appSettingsConfigData?.downloads.subtitle || null);
setSubtitleSource(appSettingsConfigData?.downloads.subtitle_source || null);
setIndexSubtitles(appSettingsConfigData?.downloads.subtitle_index || false);
// Comments
setCommentsMax(appSettingsConfig.downloads.comment_max);
setCommentsSort(appSettingsConfig.downloads.comment_sort);
setCommentsMax(appSettingsConfigData?.downloads.comment_max || null);
setCommentsSort(appSettingsConfigData?.downloads.comment_sort || '');
// Integrations
setDownloadDislikes(appSettingsConfig.downloads.integrate_ryd);
setEnableSponsorBlock(appSettingsConfig.downloads.integrate_sponsorblock);
setEnableCast(appSettingsConfig.application.enable_cast);
setDownloadDislikes(appSettingsConfigData?.downloads.integrate_ryd || false);
setEnableSponsorBlock(appSettingsConfigData?.downloads.integrate_sponsorblock || false);
setEnableCast(appSettingsConfigData?.application.enable_cast || false);
// Snapshots
setEnableSnapshots(appSettingsConfig.application.enable_snapshot);
setEnableSnapshots(appSettingsConfigData?.application.enable_snapshot || false);
setResponse({
snapshots: snapshotResponse,
appSettingsConfig,
apiToken: apiToken.token,
cookieState,
snapshots: snapshotResponseData,
appSettingsConfig: appSettingsConfigData,
apiToken: apiTokenResponseData?.token,
cookieState: cookieStateResponseData,
});
};

View File

@@ -1,12 +1,18 @@
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 loadStatsVideo, { VideoStatsType } from '../api/loader/loadStatsVideo';
import loadStatsChannel, { ChannelStatsType } from '../api/loader/loadStatsChannel';
import loadStatsPlaylist, { PlaylistStatsType } from '../api/loader/loadStatsPlaylist';
import loadStatsDownload, { DownloadStatsType } from '../api/loader/loadStatsDownload';
import loadStatsWatchProgress, {
WatchProgressStatsType,
} from '../api/loader/loadStatsWatchProgress';
import loadStatsDownloadHistory, {
DownloadHistoryStatsType,
} from '../api/loader/loadStatsDownloadHistory';
import loadStatsBiggestChannels, {
BiggestChannelsStatsType,
} from '../api/loader/loadStatsBiggestChannels';
import OverviewStats from '../components/OverviewStats';
import VideoTypeStats from '../components/VideoTypeStats';
import ApplicationStats from '../components/ApplicationStats';
@@ -17,113 +23,18 @@ import Notifications from '../components/Notifications';
import PaginationDummy from '../components/PaginationDummy';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { FileSizeUnits } from '../api/actions/updateUserConfig';
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[];
import { ApiResponseType } from '../functions/APIClient';
type DashboardStatsReponses = {
videoStats?: VideoStatsType;
channelStats?: ChannelStatsType;
playlistStats?: PlaylistStatsType;
downloadStats?: DownloadStatsType;
watchProgressStats?: WatchProgressStatsType;
downloadHistoryStats?: DownloadHistoryStatsType;
biggestChannelsStatsByCount?: BiggestChannelsStatsType;
biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
videoStats?: ApiResponseType<VideoStatsType>;
channelStats?: ApiResponseType<ChannelStatsType>;
playlistStats?: ApiResponseType<PlaylistStatsType>;
downloadStats?: ApiResponseType<DownloadStatsType>;
watchProgressStats?: ApiResponseType<WatchProgressStatsType>;
downloadHistoryStats?: ApiResponseType<DownloadHistoryStatsType>;
biggestChannelsStatsByCount?: ApiResponseType<BiggestChannelsStatsType>;
biggestChannelsStatsByDuration?: ApiResponseType<BiggestChannelsStatsType>;
biggestChannelsStatsByMediaSize?: ApiResponseType<BiggestChannelsStatsType>;
};
const SettingsDashboard = () => {
@@ -133,15 +44,15 @@ const SettingsDashboard = () => {
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;
const { data: videoStats } = response?.videoStats || {};
const { data: channelStats } = response?.channelStats || {};
const { data: playlistStats } = response?.playlistStats || {};
const { data: downloadStats } = response?.downloadStats || {};
const { data: watchProgressStats } = response?.watchProgressStats || {};
const { data: downloadHistoryStats } = response?.downloadHistoryStats || {};
const { data: biggestChannelsStatsByCount } = response?.biggestChannelsStatsByCount || {};
const { data: biggestChannelsStatsByDuration } = response?.biggestChannelsStatsByDuration || {};
const { data: biggestChannelsStatsByMediaSize } = response?.biggestChannelsStatsByMediaSize || {};
useEffect(() => {
(async () => {

View File

@@ -13,13 +13,14 @@ import createAppriseNotificationUrl, {
AppriseTaskNameType,
} from '../api/actions/createAppriseNotificationUrl';
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
import { ApiError } from '../functions/APIClient';
import { ApiError, ApiResponseType } from '../functions/APIClient';
const SettingsScheduling = () => {
const [refresh, setRefresh] = useState(false);
const [scheduleResponse, setScheduleResponse] = useState<ScheduleResponseType>([]);
const [appriseNotification, setAppriseNotification] = useState<AppriseNotificationType>();
const [scheduleResponse, setScheduleResponse] = useState<ApiResponseType<ScheduleResponseType>>();
const [appriseNotification, setAppriseNotification] =
useState<ApiResponseType<AppriseNotificationType>>();
const [updateSubscribed, setUpdateSubscribed] = useState<string | undefined>();
const [downloadPending, setDownloadPending] = useState<string | undefined>();
@@ -36,6 +37,9 @@ const SettingsScheduling = () => {
const [thumnailCheckError, setThumnailCheckError] = useState<string | null>(null);
const [zipBackupError, setZipBackupError] = useState<string | null>(null);
const { data: scheduleResponseData } = scheduleResponse ?? {};
const { data: appriseNotificationData } = appriseNotification ?? {};
useEffect(() => {
(async () => {
if (refresh) {
@@ -54,7 +58,7 @@ const SettingsScheduling = () => {
setRefresh(true);
}, []);
const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
const groupedSchedules = Object.groupBy(scheduleResponseData || [], ({ name }) => name);
console.log(groupedSchedules);
@@ -460,11 +464,11 @@ const SettingsScheduling = () => {
<div className="settings-group">
<h2>Add Notification URL</h2>
<div className="settings-item">
{!appriseNotification && <p>No notifications stored</p>}
{appriseNotification && (
{!appriseNotificationData && <p>No notifications stored</p>}
{appriseNotificationData && (
<>
<div className="description-text">
{Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
{Object.entries(appriseNotificationData)?.map(([key, { urls, title }]) => {
return (
<Fragment key={key}>
<h3>{title}</h3>

View File

@@ -58,7 +58,11 @@ const SettingsUser = () => {
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
const updatedUserConfig = await updateUserConfig(config);
setUserConfig(updatedUserConfig);
const { data: updatedUserConfigData } = updatedUserConfig;
if (updatedUserConfigData) {
setUserConfig(updatedUserConfigData);
}
};
const handlePageRefresh = () => {

View File

@@ -1,7 +1,6 @@
import { Link, useNavigate, useParams } from 'react-router-dom';
import loadVideoById from '../api/loader/loadVideoById';
import loadVideoById, { VideoResponseType } 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';
@@ -13,7 +12,7 @@ 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 loadSimilarVideosById from '../api/loader/loadSimilarVideosById';
import VideoList from '../components/VideoList';
import updateWatchedState from '../api/actions/updateWatchedState';
import humanFileSize from '../functions/humanFileSize';
@@ -27,12 +26,11 @@ 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 loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList';
import PaginationDummy from '../components/PaginationDummy';
import updateCustomPlaylist from '../api/actions/updateCustomPlaylist';
import loadCommentsbyVideoId from '../api/loader/loadCommentsbyVideoId';
import CommentBox, { CommentsType } from '../components/CommentBox';
import loadCommentsbyVideoId, { CommentsResponseType } from '../api/loader/loadCommentsbyVideoId';
import CommentBox from '../components/CommentBox';
import Button from '../components/Button';
import getApiUrl from '../configuration/getApiUrl';
import loadVideoNav, { VideoNavResponseType } from '../api/loader/loadVideoNav';
@@ -43,8 +41,8 @@ import { useAppSettingsStore } from '../stores/AppSettingsStore';
import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById';
import { FileSizeUnits } from '../api/actions/updateUserConfig';
import { useUserConfigStore } from '../stores/UserConfigStore';
import { ApiError } from '../functions/APIClient';
import NotFound from './404Page';
import NotFound from './NotFound';
import { ApiResponseType } from '../functions/APIClient';
const isInPlaylist = (videoId: string, playlist: PlaylistType) => {
return playlist.playlist_entries.some(entry => {
@@ -81,7 +79,7 @@ type PlaylistNavItemType = {
playlist_next: PlaylistNavNextItemType;
};
type PlaylistNavType = PlaylistNavItemType[];
export type PlaylistNavType = PlaylistNavItemType[];
export type SponsorBlockSegmentType = {
category: string;
@@ -101,16 +99,6 @@ export type SponsorBlockType = {
message?: string;
};
export type VideoResponseType = VideoType;
type CommentsResponseType = CommentsType[];
export type VideoCommentsResponseType = {
data: VideoType;
config: ConfigType;
playlist_nav: PlaylistNavType;
};
const Video = () => {
const { videoId } = useParams() as VideoParams;
const navigate = useNavigate();
@@ -118,7 +106,6 @@ const Video = () => {
const { appSettingsConfig } = useAppSettingsStore();
const { userConfig } = useUserConfigStore();
const [error, setError] = useState<ApiError | null>(null);
const [videoEnded, setVideoEnded] = useState(false);
const [playlistAutoplay, setPlaylistAutoplay] = useState(
localStorage.getItem('playlistAutoplay') === 'true',
@@ -132,28 +119,27 @@ const Video = () => {
const [refreshVideoList, setRefreshVideoList] = useState(false);
const [reindex, setReindex] = useState(false);
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
const [simmilarVideos, setSimmilarVideos] = useState<VideoResponseType[]>();
const [videoPlaylistNav, setVideoPlaylistNav] = useState<VideoNavResponseType[]>();
const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState<PlaylistsResponseType>();
const [commentsResponse, setCommentsResponse] = useState<CommentsResponseType>();
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
const [similarVideos, setSimilarVideos] = useState<ApiResponseType<VideoResponseType[]>>();
const [videoPlaylistNav, setVideoPlaylistNav] =
useState<ApiResponseType<VideoNavResponseType[]>>();
const [customPlaylistsResponse, setCustomPlaylistsResponse] =
useState<ApiResponseType<PlaylistsResponseType>>();
const [commentsResponse, setCommentsResponse] = useState<ApiResponseType<CommentsResponseType>>();
const { data: videoResponseData, error: videoResponseError } = videoResponse ?? {};
const { data: similarVideosResponseData } = similarVideos ?? {};
const { data: videoPlaylistNavResponseData } = videoPlaylistNav ?? {};
const { data: customPlaylistsResponseData } = customPlaylistsResponse ?? {};
const { data: commentsResponseData } = commentsResponse ?? {};
useEffect(() => {
(async () => {
if (refreshVideoList || videoId !== videoResponse?.youtube_id) {
setError(null);
try {
const videoByIdResponse = await loadVideoById(videoId);
setVideoResponse(videoByIdResponse);
} catch (err) {
if ((err as ApiError).status === 404) {
setError(err as ApiError);
} else {
console.error('Failed to fetch item:', err);
}
}
if (refreshVideoList || videoId !== videoResponseData?.youtube_id) {
const videoByIdResponse = await loadVideoById(videoId);
setVideoResponse(videoByIdResponse);
const simmilarVideosResponse = await loadSimmilarVideosById(videoId);
const similarVideosResponse = await loadSimilarVideosById(videoId);
const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' });
const videoNavResponse = await loadVideoNav(videoId);
@@ -164,7 +150,7 @@ const Video = () => {
console.log('Comments not found', e);
}
setSimmilarVideos(simmilarVideosResponse);
setSimilarVideos(similarVideosResponse);
setVideoPlaylistNav(videoNavResponse);
setCustomPlaylistsResponse(customPlaylistsResponse);
@@ -188,7 +174,7 @@ const Video = () => {
useEffect(() => {
if (videoEnded && playlistAutoplay) {
const playlist = videoPlaylistNav?.find(playlist => {
const playlist = videoPlaylistNavResponseData?.find(playlist => {
return playlist.playlist_meta.playlist_id === playlistIdForAutoplay;
});
@@ -204,19 +190,23 @@ const Video = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [videoEnded, playlistAutoplay]);
if (error) return <NotFound failType="video" />;
const errorMessage = videoResponseError?.error;
if (videoResponse === undefined) {
if (errorMessage) {
return <NotFound failType="video" />;
}
if (videoResponseData === undefined) {
return [];
}
const video = videoResponse;
const watched = videoResponse.player.watched;
const playlistNav = videoPlaylistNav;
const sponsorBlock = videoResponse.sponsorblock;
const customPlaylists = customPlaylistsResponse?.data;
const video = videoResponseData;
const watched = video.player.watched;
const playlistNav = videoPlaylistNavResponseData;
const sponsorBlock = video.sponsorblock;
const customPlaylists = customPlaylistsResponseData?.data;
const starRating = convertStarRating(video?.stats?.average_rating);
const comments = commentsResponse;
const comments = commentsResponseData;
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
console.log('playlistNav', playlistNav);
@@ -229,7 +219,7 @@ const Video = () => {
<ScrollToTopOnNavigate />
<VideoPlayer
video={videoResponse}
video={video}
sponsorBlock={sponsorBlock}
autoplay={playlistAutoplay}
onWatchStateChanged={() => {
@@ -593,7 +583,7 @@ const Video = () => {
<h3>Similar Videos</h3>
<div className="video-list grid grid-3" id="similar-videos">
<VideoList
videoList={simmilarVideos}
videoList={similarVideosResponseData}
viewLayout="grid"
refreshVideoList={setRefreshVideoList}
/>