From edcede5de6950fde8954a05789374f7222283214 Mon Sep 17 00:00:00 2001 From: MerlinScheurer Date: Thu, 20 Mar 2025 20:30:08 +0100 Subject: [PATCH] Refac APIClient to return data, error and status as object --- frontend/src/api/actions/deleteCookie.ts | 4 +- frontend/src/api/actions/updateCookie.ts | 4 +- frontend/src/api/actions/updateUserConfig.ts | 4 +- .../api/actions/updateVideoProgressById.ts | 7 +- frontend/src/api/actions/validateCookie.ts | 4 +- frontend/src/api/loader/loadApiToken.ts | 4 +- .../src/api/loader/loadAppriseNotification.ts | 4 +- .../src/api/loader/loadAppsettingsConfig.ts | 4 +- frontend/src/api/loader/loadBackupList.ts | 12 +- frontend/src/api/loader/loadChannelAggs.ts | 4 +- frontend/src/api/loader/loadChannelById.ts | 8 +- frontend/src/api/loader/loadChannelList.ts | 11 +- frontend/src/api/loader/loadChannelNav.ts | 4 +- .../src/api/loader/loadCommentsbyVideoId.ts | 5 +- frontend/src/api/loader/loadCookie.ts | 4 +- frontend/src/api/loader/loadDownloadAggs.ts | 4 +- frontend/src/api/loader/loadDownloadQueue.ts | 8 +- frontend/src/api/loader/loadNotifications.ts | 15 +- frontend/src/api/loader/loadPlaylistById.ts | 4 +- frontend/src/api/loader/loadPlaylistList.ts | 13 +- frontend/src/api/loader/loadSchedule.ts | 4 +- frontend/src/api/loader/loadSearch.ts | 17 +- .../src/api/loader/loadSimilarVideosById.ts | 8 + .../src/api/loader/loadSimmilarVideosById.ts | 7 - frontend/src/api/loader/loadSnapshots.ts | 19 ++- .../api/loader/loadStatsBiggestChannels.ts | 15 +- frontend/src/api/loader/loadStatsChannel.ts | 8 +- frontend/src/api/loader/loadStatsDownload.ts | 9 +- .../api/loader/loadStatsDownloadHistory.ts | 10 +- frontend/src/api/loader/loadStatsPlaylist.ts | 9 +- frontend/src/api/loader/loadStatsVideo.ts | 39 ++++- .../src/api/loader/loadStatsWatchProgress.ts | 22 ++- frontend/src/api/loader/loadUserAccount.ts | 4 +- frontend/src/api/loader/loadUserConfig.ts | 4 +- frontend/src/api/loader/loadVideoById.ts | 8 +- .../src/api/loader/loadVideoListByPage.ts | 6 +- frontend/src/api/loader/loadVideoNav.ts | 4 +- frontend/src/components/ApplicationStats.tsx | 4 +- .../src/components/BiggestChannelsStats.tsx | 2 +- .../src/components/DownloadHistoryStats.tsx | 2 +- .../src/components/EmbeddableVideoPlayer.tsx | 30 ++-- frontend/src/components/Filterbar.tsx | 6 +- frontend/src/components/GoogleCast.tsx | 7 +- frontend/src/components/Notifications.tsx | 31 ++-- frontend/src/components/OverviewStats.tsx | 2 +- frontend/src/components/VideoPlayer.tsx | 11 +- frontend/src/components/VideoTypeStats.tsx | 2 +- .../src/components/WatchProgressStats.tsx | 2 +- frontend/src/functions/APIClient.ts | 53 +++--- frontend/src/main.tsx | 9 +- frontend/src/pages/ChannelAbout.tsx | 29 ++-- frontend/src/pages/ChannelBase.tsx | 41 +++-- frontend/src/pages/ChannelPlaylist.tsx | 19 ++- frontend/src/pages/ChannelVideo.tsx | 33 ++-- frontend/src/pages/Channels.tsx | 29 ++-- frontend/src/pages/Download.tsx | 36 +++-- frontend/src/pages/Home.tsx | 17 +- .../src/pages/{404Page.tsx => NotFound.tsx} | 1 + frontend/src/pages/Playlist.tsx | 71 ++++---- frontend/src/pages/Playlists.tsx | 27 ++-- frontend/src/pages/Search.tsx | 50 +++--- frontend/src/pages/SettingsActions.tsx | 19 +-- frontend/src/pages/SettingsApplication.tsx | 82 ++++------ frontend/src/pages/SettingsDashboard.tsx | 153 ++++-------------- frontend/src/pages/SettingsScheduling.tsx | 18 ++- frontend/src/pages/SettingsUser.tsx | 6 +- frontend/src/pages/Video.tsx | 92 +++++------ 67 files changed, 660 insertions(+), 544 deletions(-) create mode 100644 frontend/src/api/loader/loadSimilarVideosById.ts delete mode 100644 frontend/src/api/loader/loadSimmilarVideosById.ts rename frontend/src/pages/{404Page.tsx => NotFound.tsx} (99%) diff --git a/frontend/src/api/actions/deleteCookie.ts b/frontend/src/api/actions/deleteCookie.ts index c42b8764..6c2f3da5 100644 --- a/frontend/src/api/actions/deleteCookie.ts +++ b/frontend/src/api/actions/deleteCookie.ts @@ -1,8 +1,8 @@ import APIClient from '../../functions/APIClient'; import { CookieStateType } from '../loader/loadCookie'; -const deleteCookie = async (): Promise => { - return APIClient('/api/appsettings/cookie/', { +const deleteCookie = async () => { + return APIClient('/api/appsettings/cookie/', { method: 'DELETE', }); }; diff --git a/frontend/src/api/actions/updateCookie.ts b/frontend/src/api/actions/updateCookie.ts index 7eee5a22..bf626ed7 100644 --- a/frontend/src/api/actions/updateCookie.ts +++ b/frontend/src/api/actions/updateCookie.ts @@ -1,8 +1,8 @@ import APIClient from '../../functions/APIClient'; import { CookieStateType } from '../loader/loadCookie'; -const updateCookie = async (cookie: string): Promise => { - return APIClient('/api/appsettings/cookie/', { +const updateCookie = async (cookie: string) => { + return APIClient('/api/appsettings/cookie/', { method: 'PUT', body: { cookie }, }); diff --git a/frontend/src/api/actions/updateUserConfig.ts b/frontend/src/api/actions/updateUserConfig.ts index a4261034..06eeddab 100644 --- a/frontend/src/api/actions/updateUserConfig.ts +++ b/frontend/src/api/actions/updateUserConfig.ts @@ -25,8 +25,8 @@ export type UserConfigType = { show_help_text: boolean; }; -const updateUserConfig = async (config: Partial): Promise => { - return APIClient('/api/user/me/', { +const updateUserConfig = async (config: Partial) => { + return APIClient('/api/user/me/', { method: 'POST', body: config, }); diff --git a/frontend/src/api/actions/updateVideoProgressById.ts b/frontend/src/api/actions/updateVideoProgressById.ts index e791afeb..b08cc659 100644 --- a/frontend/src/api/actions/updateVideoProgressById.ts +++ b/frontend/src/api/actions/updateVideoProgressById.ts @@ -14,11 +14,8 @@ type VideoProgressProp = { currentProgress: number; }; -const updateVideoProgressById = async ({ - youtubeId, - currentProgress, -}: VideoProgressProp): Promise => { - return APIClient(`/api/video/${youtubeId}/progress/`, { +const updateVideoProgressById = async ({ youtubeId, currentProgress }: VideoProgressProp) => { + return APIClient(`/api/video/${youtubeId}/progress/`, { method: 'POST', body: { position: currentProgress }, }); diff --git a/frontend/src/api/actions/validateCookie.ts b/frontend/src/api/actions/validateCookie.ts index 2faa2f92..a8f855b2 100644 --- a/frontend/src/api/actions/validateCookie.ts +++ b/frontend/src/api/actions/validateCookie.ts @@ -1,8 +1,8 @@ import APIClient from '../../functions/APIClient'; import { CookieStateType } from '../loader/loadCookie'; -const validateCookie = async (): Promise => { - return APIClient('/api/appsettings/cookie/', { +const validateCookie = async () => { + return APIClient('/api/appsettings/cookie/', { method: 'POST', }); }; diff --git a/frontend/src/api/loader/loadApiToken.ts b/frontend/src/api/loader/loadApiToken.ts index 86f9f455..bac6c043 100644 --- a/frontend/src/api/loader/loadApiToken.ts +++ b/frontend/src/api/loader/loadApiToken.ts @@ -4,8 +4,8 @@ type ApiTokenResponse = { token: string; }; -const loadApiToken = async (): Promise => { - return APIClient('/api/appsettings/token/'); +const loadApiToken = async () => { + return APIClient('/api/appsettings/token/'); }; export default loadApiToken; diff --git a/frontend/src/api/loader/loadAppriseNotification.ts b/frontend/src/api/loader/loadAppriseNotification.ts index 4f276dd0..5dc02062 100644 --- a/frontend/src/api/loader/loadAppriseNotification.ts +++ b/frontend/src/api/loader/loadAppriseNotification.ts @@ -19,8 +19,8 @@ export type AppriseNotificationType = { }; }; -const loadAppriseNotification = async (): Promise => { - return APIClient('/api/task/notification/'); +const loadAppriseNotification = async () => { + return APIClient('/api/task/notification/'); }; export default loadAppriseNotification; diff --git a/frontend/src/api/loader/loadAppsettingsConfig.ts b/frontend/src/api/loader/loadAppsettingsConfig.ts index df731e17..44f22e8a 100644 --- a/frontend/src/api/loader/loadAppsettingsConfig.ts +++ b/frontend/src/api/loader/loadAppsettingsConfig.ts @@ -33,8 +33,8 @@ export type AppSettingsConfigType = { }; }; -const loadAppsettingsConfig = async (): Promise => { - return APIClient('/api/appsettings/config/'); +const loadAppsettingsConfig = async () => { + return APIClient('/api/appsettings/config/'); }; export default loadAppsettingsConfig; diff --git a/frontend/src/api/loader/loadBackupList.ts b/frontend/src/api/loader/loadBackupList.ts index 64d11951..8adf7c84 100644 --- a/frontend/src/api/loader/loadBackupList.ts +++ b/frontend/src/api/loader/loadBackupList.ts @@ -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('/api/appsettings/backup/'); }; export default loadBackupList; diff --git a/frontend/src/api/loader/loadChannelAggs.ts b/frontend/src/api/loader/loadChannelAggs.ts index 435cdba3..a479ba6c 100644 --- a/frontend/src/api/loader/loadChannelAggs.ts +++ b/frontend/src/api/loader/loadChannelAggs.ts @@ -13,8 +13,8 @@ export type ChannelAggsType = { }; }; -const loadChannelAggs = async (channelId: string): Promise => { - return APIClient(`/api/channel/${channelId}/aggs/`); +const loadChannelAggs = async (channelId: string) => { + return APIClient(`/api/channel/${channelId}/aggs/`); }; export default loadChannelAggs; diff --git a/frontend/src/api/loader/loadChannelById.ts b/frontend/src/api/loader/loadChannelById.ts index a0f0c1af..d69d68c7 100644 --- a/frontend/src/api/loader/loadChannelById.ts +++ b/frontend/src/api/loader/loadChannelById.ts @@ -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 => { - return APIClient(`/api/channel/${youtubeChannelId}/`); +export type ChannelResponseType = ChannelType; + +const loadChannelById = async (youtubeChannelId: string) => { + return APIClient(`/api/channel/${youtubeChannelId}/`); }; export default loadChannelById; diff --git a/frontend/src/api/loader/loadChannelList.ts b/frontend/src/api/loader/loadChannelList.ts index 42cda1fd..928fe911 100644 --- a/frontend/src/api/loader/loadChannelList.ts +++ b/frontend/src/api/loader/loadChannelList.ts @@ -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(endpoint); }; export default loadChannelList; diff --git a/frontend/src/api/loader/loadChannelNav.ts b/frontend/src/api/loader/loadChannelNav.ts index ee0aff16..26c97c10 100644 --- a/frontend/src/api/loader/loadChannelNav.ts +++ b/frontend/src/api/loader/loadChannelNav.ts @@ -7,8 +7,8 @@ export type ChannelNavResponseType = { has_pending: boolean; }; -const loadChannelNav = async (youtubeChannelId: string): Promise => { - return APIClient(`/api/channel/${youtubeChannelId}/nav/`); +const loadChannelNav = async (youtubeChannelId: string) => { + return APIClient(`/api/channel/${youtubeChannelId}/nav/`); }; export default loadChannelNav; diff --git a/frontend/src/api/loader/loadCommentsbyVideoId.ts b/frontend/src/api/loader/loadCommentsbyVideoId.ts index 5d5484b1..395eccc4 100644 --- a/frontend/src/api/loader/loadCommentsbyVideoId.ts +++ b/frontend/src/api/loader/loadCommentsbyVideoId.ts @@ -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(`/api/video/${youtubeId}/comment/`); }; export default loadCommentsbyVideoId; diff --git a/frontend/src/api/loader/loadCookie.ts b/frontend/src/api/loader/loadCookie.ts index 8b94b977..e1921136 100644 --- a/frontend/src/api/loader/loadCookie.ts +++ b/frontend/src/api/loader/loadCookie.ts @@ -7,8 +7,8 @@ export type CookieStateType = { validated_str?: string; }; -const loadCookie = async (): Promise => { - return APIClient('/api/appsettings/cookie/'); +const loadCookie = async () => { + return APIClient('/api/appsettings/cookie/'); }; export default loadCookie; diff --git a/frontend/src/api/loader/loadDownloadAggs.ts b/frontend/src/api/loader/loadDownloadAggs.ts index 9954fa6a..935342d0 100644 --- a/frontend/src/api/loader/loadDownloadAggs.ts +++ b/frontend/src/api/loader/loadDownloadAggs.ts @@ -12,10 +12,10 @@ export type DownloadAggsType = { buckets: DownloadAggsBucket[]; }; -const loadDownloadAggs = async (showIgnored: boolean): Promise => { +const loadDownloadAggs = async (showIgnored: boolean) => { const searchParams = new URLSearchParams(); searchParams.append('filter', showIgnored ? 'ignore' : 'pending'); - return APIClient( + return APIClient( `/api/download/aggs/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`, ); }; diff --git a/frontend/src/api/loader/loadDownloadQueue.ts b/frontend/src/api/loader/loadDownloadQueue.ts index 8aa28d90..d74f4bae 100644 --- a/frontend/src/api/loader/loadDownloadQueue.ts +++ b/frontend/src/api/loader/loadDownloadQueue.ts @@ -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 => { +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(endpoint); }; export default loadDownloadQueue; diff --git a/frontend/src/api/loader/loadNotifications.ts b/frontend/src/api/loader/loadNotifications.ts index a8fb9974..7cab2b10 100644 --- a/frontend/src/api/loader/loadNotifications.ts +++ b/frontend/src/api/loader/loadNotifications.ts @@ -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(endpoint); }; export default loadNotifications; diff --git a/frontend/src/api/loader/loadPlaylistById.ts b/frontend/src/api/loader/loadPlaylistById.ts index 44007945..b800410f 100644 --- a/frontend/src/api/loader/loadPlaylistById.ts +++ b/frontend/src/api/loader/loadPlaylistById.ts @@ -26,8 +26,8 @@ export type PlaylistType = { export type PlaylistResponseType = PlaylistType; -const loadPlaylistById = async (playlistId: string | undefined): Promise => { - return APIClient(`/api/playlist/${playlistId}/`); +const loadPlaylistById = async (playlistId: string | undefined) => { + return APIClient(`/api/playlist/${playlistId}/`); }; export default loadPlaylistById; diff --git a/frontend/src/api/loader/loadPlaylistList.ts b/frontend/src/api/loader/loadPlaylistList.ts index c8779e6a..d03c9d91 100644 --- a/frontend/src/api/loader/loadPlaylistList.ts +++ b/frontend/src/api/loader/loadPlaylistList.ts @@ -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(endpoint); }; export default loadPlaylistList; diff --git a/frontend/src/api/loader/loadSchedule.ts b/frontend/src/api/loader/loadSchedule.ts index ae32c992..09cfc43c 100644 --- a/frontend/src/api/loader/loadSchedule.ts +++ b/frontend/src/api/loader/loadSchedule.ts @@ -13,8 +13,8 @@ type ScheduleType = { export type ScheduleResponseType = ScheduleType[]; -const loadSchedule = async (): Promise => { - return APIClient('/api/task/schedule/'); +const loadSchedule = async () => { + return APIClient('/api/task/schedule/'); }; export default loadSchedule; diff --git a/frontend/src/api/loader/loadSearch.ts b/frontend/src/api/loader/loadSearch.ts index fe11c969..c373a4e3 100644 --- a/frontend/src/api/loader/loadSearch.ts +++ b/frontend/src/api/loader/loadSearch.ts @@ -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(`/api/search/?query=${query}`); }; export default loadSearch; diff --git a/frontend/src/api/loader/loadSimilarVideosById.ts b/frontend/src/api/loader/loadSimilarVideosById.ts new file mode 100644 index 00000000..ddbf6502 --- /dev/null +++ b/frontend/src/api/loader/loadSimilarVideosById.ts @@ -0,0 +1,8 @@ +import APIClient from '../../functions/APIClient'; +import { VideoResponseType } from './loadVideoById'; + +const loadSimilarVideosById = async (youtubeId: string) => { + return APIClient(`/api/video/${youtubeId}/similar/`); +}; + +export default loadSimilarVideosById; diff --git a/frontend/src/api/loader/loadSimmilarVideosById.ts b/frontend/src/api/loader/loadSimmilarVideosById.ts deleted file mode 100644 index 36b1d8be..00000000 --- a/frontend/src/api/loader/loadSimmilarVideosById.ts +++ /dev/null @@ -1,7 +0,0 @@ -import APIClient from '../../functions/APIClient'; - -const loadSimmilarVideosById = async (youtubeId: string) => { - return APIClient(`/api/video/${youtubeId}/similar/`); -}; - -export default loadSimmilarVideosById; diff --git a/frontend/src/api/loader/loadSnapshots.ts b/frontend/src/api/loader/loadSnapshots.ts index 39593af2..af743757 100644 --- a/frontend/src/api/loader/loadSnapshots.ts +++ b/frontend/src/api/loader/loadSnapshots.ts @@ -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('/api/appsettings/snapshot/'); }; export default loadSnapshots; diff --git a/frontend/src/api/loader/loadStatsBiggestChannels.ts b/frontend/src/api/loader/loadStatsBiggestChannels.ts index ec522277..b2e4c218 100644 --- a/frontend/src/api/loader/loadStatsBiggestChannels.ts +++ b/frontend/src/api/loader/loadStatsBiggestChannels.ts @@ -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( + `/api/stats/biggestchannels/?${searchParams.toString()}`, + ); }; export default loadStatsBiggestChannels; diff --git a/frontend/src/api/loader/loadStatsChannel.ts b/frontend/src/api/loader/loadStatsChannel.ts index e149a658..135a72eb 100644 --- a/frontend/src/api/loader/loadStatsChannel.ts +++ b/frontend/src/api/loader/loadStatsChannel.ts @@ -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('/api/stats/channel/'); }; export default loadStatsChannel; diff --git a/frontend/src/api/loader/loadStatsDownload.ts b/frontend/src/api/loader/loadStatsDownload.ts index cec35971..30266e25 100644 --- a/frontend/src/api/loader/loadStatsDownload.ts +++ b/frontend/src/api/loader/loadStatsDownload.ts @@ -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('/api/stats/download/'); }; export default loadStatsDownload; diff --git a/frontend/src/api/loader/loadStatsDownloadHistory.ts b/frontend/src/api/loader/loadStatsDownloadHistory.ts index 095504ab..3be7e7f9 100644 --- a/frontend/src/api/loader/loadStatsDownloadHistory.ts +++ b/frontend/src/api/loader/loadStatsDownloadHistory.ts @@ -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('/api/stats/downloadhist/'); }; export default loadStatsDownloadHistory; diff --git a/frontend/src/api/loader/loadStatsPlaylist.ts b/frontend/src/api/loader/loadStatsPlaylist.ts index 22f4dd01..b2249264 100644 --- a/frontend/src/api/loader/loadStatsPlaylist.ts +++ b/frontend/src/api/loader/loadStatsPlaylist.ts @@ -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('/api/stats/playlist/'); }; export default loadStatsPlaylist; diff --git a/frontend/src/api/loader/loadStatsVideo.ts b/frontend/src/api/loader/loadStatsVideo.ts index d1812e21..7d0455d3 100644 --- a/frontend/src/api/loader/loadStatsVideo.ts +++ b/frontend/src/api/loader/loadStatsVideo.ts @@ -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('/api/stats/video/'); }; export default loadStatsVideo; diff --git a/frontend/src/api/loader/loadStatsWatchProgress.ts b/frontend/src/api/loader/loadStatsWatchProgress.ts index 3fdf58f9..34ac3e29 100644 --- a/frontend/src/api/loader/loadStatsWatchProgress.ts +++ b/frontend/src/api/loader/loadStatsWatchProgress.ts @@ -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('/api/stats/watch/'); }; export default loadStatsWatchProgress; diff --git a/frontend/src/api/loader/loadUserAccount.ts b/frontend/src/api/loader/loadUserAccount.ts index bc452cc2..4501e3df 100644 --- a/frontend/src/api/loader/loadUserAccount.ts +++ b/frontend/src/api/loader/loadUserAccount.ts @@ -10,8 +10,8 @@ export type UserAccountType = { last_login: string; }; -const loadUserAccount = async (): Promise => { - return APIClient('/api/user/account/'); +const loadUserAccount = async () => { + return APIClient('/api/user/account/'); }; export default loadUserAccount; diff --git a/frontend/src/api/loader/loadUserConfig.ts b/frontend/src/api/loader/loadUserConfig.ts index fbf96dd3..646157e0 100644 --- a/frontend/src/api/loader/loadUserConfig.ts +++ b/frontend/src/api/loader/loadUserConfig.ts @@ -1,8 +1,8 @@ import { UserConfigType } from '../actions/updateUserConfig'; import APIClient from '../../functions/APIClient'; -const loadUserMeConfig = async (): Promise => { - return APIClient('/api/user/me/'); +const loadUserMeConfig = async () => { + return APIClient('/api/user/me/'); }; export default loadUserMeConfig; diff --git a/frontend/src/api/loader/loadVideoById.ts b/frontend/src/api/loader/loadVideoById.ts index ed01831c..2fee7a48 100644 --- a/frontend/src/api/loader/loadVideoById.ts +++ b/frontend/src/api/loader/loadVideoById.ts @@ -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 => { - return APIClient(`/api/video/${youtubeId}/`); +export type VideoResponseType = VideoType; + +const loadVideoById = async (youtubeId: string) => { + return APIClient(`/api/video/${youtubeId}/`); }; export default loadVideoById; diff --git a/frontend/src/api/loader/loadVideoListByPage.ts b/frontend/src/api/loader/loadVideoListByPage.ts index c2c019da..06066f77 100644 --- a/frontend/src/api/loader/loadVideoListByPage.ts +++ b/frontend/src/api/loader/loadVideoListByPage.ts @@ -21,9 +21,7 @@ type FilterType = { type?: VideoTypes; }; -const loadVideoListByFilter = async ( - filter: FilterType, -): Promise => { +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(endpoint); }; export default loadVideoListByFilter; diff --git a/frontend/src/api/loader/loadVideoNav.ts b/frontend/src/api/loader/loadVideoNav.ts index 46cee633..ea22db99 100644 --- a/frontend/src/api/loader/loadVideoNav.ts +++ b/frontend/src/api/loader/loadVideoNav.ts @@ -25,8 +25,8 @@ export type VideoNavResponseType = { }; }; -const loadVideoNav = async (youtubeVideoId: string): Promise => { - return APIClient(`/api/video/${youtubeVideoId}/nav/`); +const loadVideoNav = async (youtubeVideoId: string) => { + return APIClient(`/api/video/${youtubeVideoId}/nav/`); }; export default loadVideoNav; diff --git a/frontend/src/components/ApplicationStats.tsx b/frontend/src/components/ApplicationStats.tsx index eaddfbd9..da738b40 100644 --- a/frontend/src/components/ApplicationStats.tsx +++ b/frontend/src/components/ApplicationStats.tsx @@ -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; diff --git a/frontend/src/components/BiggestChannelsStats.tsx b/frontend/src/components/BiggestChannelsStats.tsx index 7ccf32f5..df8ca978 100644 --- a/frontend/src/components/BiggestChannelsStats.tsx +++ b/frontend/src/components/BiggestChannelsStats.tsx @@ -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; diff --git a/frontend/src/components/DownloadHistoryStats.tsx b/frontend/src/components/DownloadHistoryStats.tsx index 74d440df..fdc90fb0 100644 --- a/frontend/src/components/DownloadHistoryStats.tsx +++ b/frontend/src/components/DownloadHistoryStats.tsx @@ -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; diff --git a/frontend/src/components/EmbeddableVideoPlayer.tsx b/frontend/src/components/EmbeddableVideoPlayer.tsx index 80b0906f..6ad0a791 100644 --- a/frontend/src/components/EmbeddableVideoPlayer.tsx +++ b/frontend/src/components/EmbeddableVideoPlayer.tsx @@ -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(); + const [videoResponse, setVideoResponse] = useState>(); const [playlists, setPlaylists] = useState(); + 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
; } - 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) => {
) => { const updatedUserConfig = await updateUserConfig(config); - setUserConfig(updatedUserConfig); + const { data: updatedUserConfigData } = updatedUserConfig; + + if (updatedUserConfigData) { + setUserConfig(updatedUserConfigData); + } }; return ( diff --git a/frontend/src/components/GoogleCast.tsx b/frontend/src/components/GoogleCast.tsx index 210c7203..831c695d 100644 --- a/frontend/src/components/GoogleCast.tsx +++ b/frontend/src/components/GoogleCast.tsx @@ -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); } } diff --git a/frontend/src/components/Notifications.tsx b/frontend/src/components/Notifications.tsx index bfc107da..37b0a551 100644 --- a/frontend/src/components/Notifications.tsx +++ b/frontend/src/components/Notifications.tsx @@ -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([]); + const [notificationResponse, setNotificationResponse] = + useState>(); + + 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 => (
{ return (Number(progress) * 100).toFixed(2) ?? '0'; diff --git a/frontend/src/functions/APIClient.ts b/frontend/src/functions/APIClient.ts index 2c4a4110..c0656a63 100644 --- a/frontend/src/functions/APIClient.ts +++ b/frontend/src/functions/APIClient.ts @@ -15,10 +15,20 @@ export interface ApiError { message: string; } -const APIClient = async ( +export type ResponseErrorType = { + error: string; +}; + +export type ApiResponseType = { + data?: T; + error?: ResponseErrorType; + status: number; +}; + +const APIClient = async ( endpoint: string, { method = 'GET', body, headers = {}, ...options }: ApiClientOptions = {}, -) => { +): Promise> => { 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; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index e7dce8bc..53868ba0 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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: , + errorElement: , }, ], { basename: import.meta.env.BASE_URL }, diff --git a/frontend/src/pages/ChannelAbout.tsx b/frontend/src/pages/ChannelAbout.tsx index b346964b..1ad1cf46 100644 --- a/frontend/src/pages/ChannelAbout.tsx +++ b/frontend/src/pages/ChannelAbout.tsx @@ -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(); + const [channelResponse, setChannelResponse] = useState>(); const [downloadFormat, setDownloadFormat] = useState(null); const [autoDeleteAfter, setAutoDeleteAfter] = useState(null); @@ -55,24 +55,31 @@ const ChannelAbout = () => { const [pageSizeShorts, setPageSizeShorts] = useState(null); const [pageSizeStreams, setPageSizeStreams] = useState(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); diff --git a/frontend/src/pages/ChannelBase.tsx b/frontend/src/pages/ChannelBase.tsx index cb63e0e7..05bf21af 100644 --- a/frontend/src/pages/ChannelBase.tsx +++ b/frontend/src/pages/ChannelBase.tsx @@ -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(null); const isAdmin = useIsAdmin(); - const [channelResponse, setChannelResponse] = useState(); - const [channelNav, setChannelNav] = useState(); + const [channelResponse, setChannelResponse] = useState>(); + const [channelNav, setChannelNav] = useState>(); 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 ; + const errorMessage = channelResponseError?.error; + + if (errorMessage) { + return ; + } if (!channelId) { return []; diff --git a/frontend/src/pages/ChannelPlaylist.tsx b/frontend/src/pages/ChannelPlaylist.tsx index 5c592dbf..c3730e56 100644 --- a/frontend/src/pages/ChannelPlaylist.tsx +++ b/frontend/src/pages/ChannelPlaylist.tsx @@ -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(); + const [playlistsResponse, setPlaylistsResponse] = + useState>(); - 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) => { const updatedUserConfig = await updateUserConfig(config); - setUserConfig(updatedUserConfig); + const { data: updatedUserConfigData } = updatedUserConfig; + + if (updatedUserConfigData) { + setUserConfig(updatedUserConfigData); + } }; useEffect(() => { diff --git a/frontend/src/pages/ChannelVideo.tsx b/frontend/src/pages/ChannelVideo.tsx index 7a2cc5c4..a6f3f4ed 100644 --- a/frontend/src/pages/ChannelVideo.tsx +++ b/frontend/src/pages/ChannelVideo.tsx @@ -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(); - const [videoResponse, setVideoReponse] = useState(); - const [videoAggsResponse, setVideoAggsResponse] = useState(); + const [channelResponse, setChannelResponse] = useState>(); + const [videoResponse, setVideoReponse] = + useState>(); + const [videoAggsResponse, setVideoAggsResponse] = useState>(); - 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} />
- {videoAggsResponse && ( + {videoAggs && ( <>

- {videoAggsResponse.total_items.value} videos{' '} - |{' '} - {videoAggsResponse.total_duration.value_str} playback{' '} + {videoAggs.total_items.value} videos |{' '} + {videoAggs.total_duration.value_str} playback{' '} | Total size{' '} - {humanFileSize(videoAggsResponse.total_size.value, useSiUnits)} + {humanFileSize(videoAggs.total_size.value, useSiUnits)}

- {/* {hasChannels &&

Total channels: {channelCount}

} */}
{!hasChannels &&

No channels found...

} diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx index 950bb18a..9e096916 100644 --- a/frontend/src/pages/Download.tsx +++ b/frontend/src/pages/Download.tsx @@ -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(); - const [downloadAggsResponse, setDownloadAggsResponse] = useState(); + const [downloadResponse, setDownloadResponse] = useState>(); + const [downloadAggsResponse, setDownloadAggsResponse] = + useState>(); - 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) => { 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); })(); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 446e9ab8..45a5b49b 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -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(); + const [videoResponse, setVideoReponse] = + useState>(); const [continueVideoResponse, setContinueVideoResponse] = - useState(); + useState>(); - 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}` : ''; diff --git a/frontend/src/pages/404Page.tsx b/frontend/src/pages/NotFound.tsx similarity index 99% rename from frontend/src/pages/404Page.tsx rename to frontend/src/pages/NotFound.tsx index 1b75f7ab..46dfb7a7 100644 --- a/frontend/src/pages/404Page.tsx +++ b/frontend/src/pages/NotFound.tsx @@ -4,6 +4,7 @@ import Routes from '../configuration/routes/RouteList'; const NotFound = ({ failType = 'page' }) => { useColours(); + return ( <> 404 | Not found diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx index 6af665ec..869aeab8 100644 --- a/frontend/src/pages/Playlist.tsx +++ b/frontend/src/pages/Playlist.tsx @@ -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(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(); - const [channelResponse, setChannelResponse] = useState(); - const [videoResponse, setVideoResponse] = useState(); + const [playlistResponse, setPlaylistResponse] = useState>(); + const [channelResponse, setChannelResponse] = useState>(); + const [videoResponse, setVideoResponse] = useState>(); - 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 ; + const errorMessage = playlistResponseError?.error; + + if (errorMessage) { + return ; + } if (!playlist || !playlistId) return []; diff --git a/frontend/src/pages/Playlists.tsx b/frontend/src/pages/Playlists.tsx index d7e4539d..4bc5df27 100644 --- a/frontend/src/pages/Playlists.tsx +++ b/frontend/src/pages/Playlists.tsx @@ -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(); + const [playlistResponse, setPlaylistReponse] = useState>(); - 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) => { const updatedUserConfig = await updateUserConfig(config); - setUserConfig(updatedUserConfig); + const { data: updatedUserConfigData } = updatedUserConfig; + + if (updatedUserConfigData) { + setUserConfig(updatedUserConfigData); + } }; return ( diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index 111ca82e..b2718d79 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -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 = { + 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(''); - const [searchResults, setSearchResults] = 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 { 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; diff --git a/frontend/src/pages/SettingsActions.tsx b/frontend/src/pages/SettingsActions.tsx index 9bb532ab..f56383d3 100644 --- a/frontend/src/pages/SettingsActions.tsx +++ b/frontend/src/pages/SettingsActions.tsx @@ -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(); + const [backupListResponse, setBackupListResponse] = useState>(); - const backups = backupListResponse; + const { data: backupListResponseData } = backupListResponse ?? {}; + + const backups = backupListResponseData; const hasBackups = !!backups && backups?.length > 0; useEffect(() => { diff --git a/frontend/src/pages/SettingsApplication.tsx b/frontend/src/pages/SettingsApplication.tsx index c47d776c..f8e1a39b 100644 --- a/frontend/src/pages/SettingsApplication.tsx +++ b/frontend/src/pages/SettingsApplication.tsx @@ -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, }); }; diff --git a/frontend/src/pages/SettingsDashboard.tsx b/frontend/src/pages/SettingsDashboard.tsx index 450f89b2..0276bd09 100644 --- a/frontend/src/pages/SettingsDashboard.tsx +++ b/frontend/src/pages/SettingsDashboard.tsx @@ -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; + channelStats?: ApiResponseType; + playlistStats?: ApiResponseType; + downloadStats?: ApiResponseType; + watchProgressStats?: ApiResponseType; + downloadHistoryStats?: ApiResponseType; + biggestChannelsStatsByCount?: ApiResponseType; + biggestChannelsStatsByDuration?: ApiResponseType; + biggestChannelsStatsByMediaSize?: ApiResponseType; }; 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 () => { diff --git a/frontend/src/pages/SettingsScheduling.tsx b/frontend/src/pages/SettingsScheduling.tsx index d1ebd685..b56f7697 100644 --- a/frontend/src/pages/SettingsScheduling.tsx +++ b/frontend/src/pages/SettingsScheduling.tsx @@ -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([]); - const [appriseNotification, setAppriseNotification] = useState(); + const [scheduleResponse, setScheduleResponse] = useState>(); + const [appriseNotification, setAppriseNotification] = + useState>(); const [updateSubscribed, setUpdateSubscribed] = useState(); const [downloadPending, setDownloadPending] = useState(); @@ -36,6 +37,9 @@ const SettingsScheduling = () => { const [thumnailCheckError, setThumnailCheckError] = useState(null); const [zipBackupError, setZipBackupError] = useState(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 = () => {

Add Notification URL

- {!appriseNotification &&

No notifications stored

} - {appriseNotification && ( + {!appriseNotificationData &&

No notifications stored

} + {appriseNotificationData && ( <>
- {Object.entries(appriseNotification)?.map(([key, { urls, title }]) => { + {Object.entries(appriseNotificationData)?.map(([key, { urls, title }]) => { return (

{title}

diff --git a/frontend/src/pages/SettingsUser.tsx b/frontend/src/pages/SettingsUser.tsx index 8f900ff5..da46c26c 100644 --- a/frontend/src/pages/SettingsUser.tsx +++ b/frontend/src/pages/SettingsUser.tsx @@ -58,7 +58,11 @@ const SettingsUser = () => { const handleUserConfigUpdate = async (config: Partial) => { const updatedUserConfig = await updateUserConfig(config); - setUserConfig(updatedUserConfig); + const { data: updatedUserConfigData } = updatedUserConfig; + + if (updatedUserConfigData) { + setUserConfig(updatedUserConfigData); + } }; const handlePageRefresh = () => { diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx index 127c23eb..2c9b47ab 100644 --- a/frontend/src/pages/Video.tsx +++ b/frontend/src/pages/Video.tsx @@ -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(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(); - const [simmilarVideos, setSimmilarVideos] = useState(); - const [videoPlaylistNav, setVideoPlaylistNav] = useState(); - const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState(); - const [commentsResponse, setCommentsResponse] = useState(); + const [videoResponse, setVideoResponse] = useState>(); + const [similarVideos, setSimilarVideos] = useState>(); + const [videoPlaylistNav, setVideoPlaylistNav] = + useState>(); + const [customPlaylistsResponse, setCustomPlaylistsResponse] = + useState>(); + const [commentsResponse, setCommentsResponse] = useState>(); + + 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 ; + const errorMessage = videoResponseError?.error; - if (videoResponse === undefined) { + if (errorMessage) { + return ; + } + + 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 = () => { { @@ -593,7 +583,7 @@ const Video = () => {

Similar Videos