From e06e1be433267ae9f64792fc7d3b12d37676a4e9 Mon Sep 17 00:00:00 2001 From: MerlinScheurer Date: Mon, 10 Mar 2025 20:26:40 +0100 Subject: [PATCH 01/17] Add TA_HOST example and compose with port --- README.md | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bbc91609..9a8c6f49 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ All environment variables are explained in detail in the docs [here](https://doc **TubeArchivist**: | Environment Var | Value | | | ----------- | ----------- | ----------- | -| TA_HOST | Server IP or hostname | Required | +| TA_HOST | Server IP or hostname `http://tubearchivist.local:8000` | Required | | TA_USERNAME | Initial username when logging into TA | Required | | TA_PASSWORD | Initial password when logging into TA | Required | | ELASTIC_PASSWORD | Password for ElasticSearch | Required | diff --git a/docker-compose.yml b/docker-compose.yml index e7a5c4e3..42be78db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - REDIS_CON=redis://archivist-redis:6379 - HOST_UID=1000 - HOST_GID=1000 - - TA_HOST=http://tubearchivist.local # set your host name with protocol and port + - TA_HOST=http://tubearchivist.local:8000 # set your host name with protocol and port - TA_USERNAME=tubearchivist # your initial TA credentials - TA_PASSWORD=verysecret # your initial TA credentials - ELASTIC_PASSWORD=verysecret # set password for Elasticsearch From 862a854e64359d472f42ee2a73f46666d3431d4e Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Mar 2025 22:10:17 +0700 Subject: [PATCH 02/17] add health API endpoint, #887 --- backend/common/src/health.py | 11 ----------- backend/common/urls.py | 5 +++++ backend/common/views.py | 9 +++++++++ backend/config/settings.py | 1 - 4 files changed, 14 insertions(+), 12 deletions(-) delete mode 100644 backend/common/src/health.py diff --git a/backend/common/src/health.py b/backend/common/src/health.py deleted file mode 100644 index 001a0216..00000000 --- a/backend/common/src/health.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.http import HttpResponse - - -class HealthCheckMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - if request.path == "/health": - return HttpResponse("ok") - return self.get_response(request) diff --git a/backend/common/urls.py b/backend/common/urls.py index e8a9fe94..92ed43b9 100644 --- a/backend/common/urls.py +++ b/backend/common/urls.py @@ -25,4 +25,9 @@ urlpatterns = [ views.NotificationView.as_view(), name="api-notification", ), + path( + "health/", + views.HealthCheck.as_view(), + name="api-health", + ), ] diff --git a/backend/common/views.py b/backend/common/views.py index 4a30b7e0..f7ba9973 100644 --- a/backend/common/views.py +++ b/backend/common/views.py @@ -20,6 +20,7 @@ from common.src.watched import WatchState from common.views_base import AdminOnly, ApiBaseView from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.response import Response +from rest_framework.views import APIView from task.tasks import check_reindex @@ -199,3 +200,11 @@ class NotificationView(ApiBaseView): response_serializer = NotificationSerializer(notifications, many=True) return Response(response_serializer.data) + + +class HealthCheck(APIView): + """health check view, no auth needed""" + + def get(self, request): + """health check, no auth needed""" + return Response("OK", status=200) diff --git a/backend/config/settings.py b/backend/config/settings.py index 8e9aab9b..1e2355ed 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -82,7 +82,6 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "common.src.health.HealthCheckMiddleware", ] ROOT_URLCONF = "config.urls" From 7abcfcc579e9a040fff9cbb97a80479790bb1a44 Mon Sep 17 00:00:00 2001 From: MerlinScheurer Date: Fri, 14 Mar 2025 09:08:40 +0100 Subject: [PATCH 03/17] Add 'having issues' help text to login after 10 seconds --- frontend/src/pages/Login.tsx | 34 ++++++++++++++++++++++++++++++++++ frontend/src/style.css | 4 ++++ 2 files changed, 38 insertions(+) diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 9caf27ee..1986cefd 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -15,6 +15,7 @@ const Login = () => { const [password, setPassword] = useState(''); const [saveLogin, setSaveLogin] = useState(false); const [waitingForBackend, setWaitingForBackend] = useState(false); + const [waitedCount, setWaitedCount] = useState(0); const [errorMessage, setErrorMessage] = useState(null); const handleSubmit = async (event: { preventDefault: () => void }) => { @@ -40,6 +41,8 @@ const Login = () => { }; useEffect(() => { + let retryCount = 0; + const backendCheckInterval = setInterval(async () => { try { const auth = await loadAuth(); @@ -59,6 +62,8 @@ const Login = () => { } } catch (error) { console.log('Checking backend availability: ', error); + retryCount += 1; + setWaitedCount(retryCount); setWaitingForBackend(true); } }, 1000); @@ -137,6 +142,35 @@ const Login = () => { {!waitingForBackend && } +

Archive view page size

@@ -113,18 +129,40 @@ const SettingsUser = () => {
+

Show help text

+
+ +
+
+

File size units:

+
+ + +
+ {isAdmin && ( <>
diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx index 2d1d81fe..ba659e32 100644 --- a/frontend/src/pages/Video.tsx +++ b/frontend/src/pages/Video.tsx @@ -41,6 +41,8 @@ import ToggleConfig from '../components/ToggleConfig'; import { PlaylistType } from '../api/loader/loadPlaylistById'; import { useAppSettingsStore } from '../stores/AppSettingsStore'; import updateDownloadQueueStatusById from '../api/actions/updateDownloadQueueStatusById'; +import { FileSizeUnits } from '../api/actions/updateUserConfig'; +import { useUserConfigStore } from '../stores/UserConfigStore'; const isInPlaylist = (videoId: string, playlist: PlaylistType) => { return playlist.playlist_entries.some(entry => { @@ -112,6 +114,7 @@ const Video = () => { const navigate = useNavigate(); const isAdmin = useIsAdmin(); const { appSettingsConfig } = useAppSettingsStore(); + const { userConfig } = useUserConfigStore(); const [videoEnded, setVideoEnded] = useState(false); const [playlistAutoplay, setPlaylistAutoplay] = useState( @@ -199,6 +202,7 @@ const Video = () => { const customPlaylists = customPlaylistsResponse?.data; const starRating = convertStarRating(video?.stats?.average_rating); const comments = commentsResponse; + const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric; console.log('playlistNav', playlistNav); @@ -439,14 +443,14 @@ const Video = () => {
- {video.media_size &&

File size: {humanFileSize(video.media_size)}

} + {video.media_size &&

File size: {humanFileSize(video.media_size, useSiUnits)}

} {video.streams && video.streams.map(stream => { return (

{capitalizeFirstLetter(stream.type)}: {stream.codec}{' '} - {humanFileSize(stream.bitrate)}/s + {humanFileSize(stream.bitrate, useSiUnits)}/s {stream.width && ( <> | {stream.width}x{stream.height} diff --git a/frontend/src/stores/UserConfigStore.ts b/frontend/src/stores/UserConfigStore.ts index 609f4b7b..20b2a417 100644 --- a/frontend/src/stores/UserConfigStore.ts +++ b/frontend/src/stores/UserConfigStore.ts @@ -18,6 +18,7 @@ export const useUserConfigStore = create(set => ({ view_style_playlist: 'grid', grid_items: 3, hide_watched: false, + file_size_unit: 'binary', show_ignored_only: false, show_subed_only: false, show_help_text: true, From a4824497ef9435738b2b835abacd4fd06e8881ee Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 15 Mar 2025 16:56:18 +0100 Subject: [PATCH 11/17] handle 404 pages, #890 --- frontend/src/functions/APIClient.ts | 7 +++++ frontend/src/main.tsx | 5 ++++ frontend/src/pages/404Page.tsx | 22 ++++++++++++++ frontend/src/pages/ChannelBase.tsx | 19 ++++++++++-- frontend/src/pages/Playlist.tsx | 45 +++++++++++++++++------------ frontend/src/pages/Video.tsx | 19 ++++++++++-- 6 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 frontend/src/pages/404Page.tsx diff --git a/frontend/src/functions/APIClient.ts b/frontend/src/functions/APIClient.ts index c01ba8b8..2c4a4110 100644 --- a/frontend/src/functions/APIClient.ts +++ b/frontend/src/functions/APIClient.ts @@ -55,6 +55,13 @@ 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 diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4fc91d7a..e7dce8bc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -27,6 +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'; const router = createBrowserRouter( [ @@ -145,6 +146,10 @@ const router = createBrowserRouter( element: , errorElement: , }, + { + path: '*', + element: , + }, ], { basename: import.meta.env.BASE_URL }, ); diff --git a/frontend/src/pages/404Page.tsx b/frontend/src/pages/404Page.tsx new file mode 100644 index 00000000..1b75f7ab --- /dev/null +++ b/frontend/src/pages/404Page.tsx @@ -0,0 +1,22 @@ +import { Link } from 'react-router-dom'; +import useColours from '../configuration/colours/useColours'; +import Routes from '../configuration/routes/RouteList'; + +const NotFound = ({ failType = 'page' }) => { + useColours(); + return ( + <> + 404 | Not found +

+

Oops!

+

+ 404 + : That {failType} does not exist. +

+ Go Home +
+ + ); +}; + +export default NotFound; diff --git a/frontend/src/pages/ChannelBase.tsx b/frontend/src/pages/ChannelBase.tsx index 2da7c552..cb63e0e7 100644 --- a/frontend/src/pages/ChannelBase.tsx +++ b/frontend/src/pages/ChannelBase.tsx @@ -8,6 +8,8 @@ import ChannelBanner from '../components/ChannelBanner'; import loadChannelNav, { ChannelNavResponseType } from '../api/loader/loadChannelNav'; import loadChannelById from '../api/loader/loadChannelById'; import useIsAdmin from '../functions/useIsAdmin'; +import { ApiError } from '../functions/APIClient'; +import NotFound from './404Page'; type ChannelParams = { channelId: string; @@ -18,6 +20,7 @@ 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(); @@ -29,14 +32,24 @@ const ChannelBase = () => { 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 channelNavResponse = await loadChannelNav(channelId); - const channelResponse = await loadChannelById(channelId); - - setChannelResponse(channelResponse); setChannelNav(channelNavResponse); })(); }, [channelId]); + if (error) return ; + if (!channelId) { return []; } diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx index 0ee310e8..6af665ec 100644 --- a/frontend/src/pages/Playlist.tsx +++ b/frontend/src/pages/Playlist.tsx @@ -23,6 +23,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'; export type VideoResponseType = { data?: VideoType[]; @@ -31,6 +33,7 @@ export type VideoResponseType = { const Playlist = () => { const { playlistId } = useParams(); + const [error, setError] = useState(null); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const videoId = searchParams.get('videoId'); @@ -66,22 +69,28 @@ const Playlist = () => { useEffect(() => { (async () => { - const playlist = await loadPlaylistById(playlistId); - const video = await loadVideoListByFilter({ - playlist: playlistId, - page: currentPage, - watch: hideWatched ? 'unwatched' : undefined, - }); - - const isCustomPlaylist = playlist?.playlist_type === 'custom'; - if (!isCustomPlaylist) { - const channel = await loadChannelById(playlist.playlist_channel_id); - - setChannelResponse(channel); + 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); + } } - - setPlaylistResponse(playlist); - setVideoResponse(video); setRefresh(false); })(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -94,9 +103,9 @@ const Playlist = () => { videoId, ]); - if (!playlistId || !playlist) { - return `Playlist ${playlistId} not found!`; - } + if (error) return ; + + if (!playlist || !playlistId) return []; const isCustomPlaylist = playlist.playlist_type === 'custom'; diff --git a/frontend/src/pages/Video.tsx b/frontend/src/pages/Video.tsx index ba659e32..127c23eb 100644 --- a/frontend/src/pages/Video.tsx +++ b/frontend/src/pages/Video.tsx @@ -43,6 +43,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'; const isInPlaylist = (videoId: string, playlist: PlaylistType) => { return playlist.playlist_entries.some(entry => { @@ -116,6 +118,7 @@ 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', @@ -138,7 +141,18 @@ const Video = () => { useEffect(() => { (async () => { if (refreshVideoList || videoId !== videoResponse?.youtube_id) { - const videoByIdResponse = await loadVideoById(videoId); + 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); + } + } + const simmilarVideosResponse = await loadSimmilarVideosById(videoId); const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' }); const videoNavResponse = await loadVideoNav(videoId); @@ -150,7 +164,6 @@ const Video = () => { console.log('Comments not found', e); } - setVideoResponse(videoByIdResponse); setSimmilarVideos(simmilarVideosResponse); setVideoPlaylistNav(videoNavResponse); setCustomPlaylistsResponse(customPlaylistsResponse); @@ -191,6 +204,8 @@ const Video = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [videoEnded, playlistAutoplay]); + if (error) return ; + if (videoResponse === undefined) { return []; } From 52083e6fb7d62ccd1b98c9b9fb2952df7b21f2b1 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 19 Mar 2025 07:53:30 +0100 Subject: [PATCH 12/17] fix redis connection failure error message --- backend/config/management/commands/ta_connection.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/config/management/commands/ta_connection.py b/backend/config/management/commands/ta_connection.py index 1b8158e6..ae2f0b08 100644 --- a/backend/config/management/commands/ta_connection.py +++ b/backend/config/management/commands/ta_connection.py @@ -58,7 +58,12 @@ class Command(BaseCommand): message = " 🗙 Redis connection failed" self.stdout.write(self.style.ERROR(f"{message}")) - RedisArchivist().exec("PING") + try: + redis_conn.execute_command("PING") + except Exception as err: # pylint: disable=broad-except + message = f" 🗙 {type(err).__name__}: {err}" + self.stdout.write(self.style.ERROR(f"{message}")) + sleep(60) raise CommandError(message) From edcede5de6950fde8954a05789374f7222283214 Mon Sep 17 00:00:00 2001 From: MerlinScheurer Date: Thu, 20 Mar 2025 20:30:08 +0100 Subject: [PATCH 13/17] 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

From 7fdb93f183f27f40494ba6fb3d27d1d7d3a9806d Mon Sep 17 00:00:00 2001 From: MerlinScheurer Date: Thu, 20 Mar 2025 20:30:32 +0100 Subject: [PATCH 14/17] Fix formatting in frontend --- frontend/src/pages/Channels.tsx | 2 +- frontend/src/pages/Download.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 66cf1b10..5301d6c0 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -62,7 +62,7 @@ const Channels = () => { const handleUserConfigUpdate = async (config: Partial) => { const updatedUserConfig = await updateUserConfig(config); - const { data: updatedUserConfigData } = updatedUserConfig ; + const { data: updatedUserConfigData } = updatedUserConfig; if (updatedUserConfigData) { setUserConfig(updatedUserConfigData); diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx index 9e096916..b32c276b 100644 --- a/frontend/src/pages/Download.tsx +++ b/frontend/src/pages/Download.tsx @@ -89,7 +89,7 @@ const Download = () => { const handleUserConfigUpdate = async (config: Partial) => { const updatedUserConfig = await updateUserConfig(config); - const { data: updatedUserConfigData } = updatedUserConfig ; + const { data: updatedUserConfigData } = updatedUserConfig; if (updatedUserConfigData) { setUserConfig(updatedUserConfigData); From 512f6618239f926a1b01b6bb630a7079e92681b1 Mon Sep 17 00:00:00 2001 From: MerlinScheurer Date: Sat, 22 Mar 2025 10:36:35 +0100 Subject: [PATCH 15/17] Add sourcemaps for easier debugging after build --- frontend/vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index be6d249e..1065e7ce 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,4 +15,7 @@ export default defineConfig({ usePolling: true, }, }, + build: { + sourcemap: true, + }, }); From 164f3d2a22e4d9be1292225400d88311a3e4a417 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 24 Mar 2025 21:53:37 +0100 Subject: [PATCH 16/17] bump requirements --- backend/requirements-dev.txt | 6 +++--- backend/requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 9e276c48..58cfeac2 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,8 +1,8 @@ -r requirements.txt -ipython==9.0.1 -pre-commit==4.1.0 +ipython==9.0.2 +pre-commit==4.2.0 pylint-django==2.6.1 -pylint==3.3.4 +pylint==3.3.6 pytest-django==4.10.0 pytest==8.3.5 python-dotenv==1.0.1 diff --git a/backend/requirements.txt b/backend/requirements.txt index 61848a09..a948144c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,4 +12,4 @@ requests==2.32.3 ryd-client==0.0.6 uvicorn==0.34.0 whitenoise==6.9.0 -yt-dlp[default]==2025.2.19 +yt-dlp[default]==2025.3.21 From 17aa693fdf998bbe1345b86940648e00668a44e6 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 24 Mar 2025 21:59:35 +0100 Subject: [PATCH 17/17] fix backend tasks messages notification types --- backend/task/tasks.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/task/tasks.py b/backend/task/tasks.py index 2f83b4c5..31d7eb83 100644 --- a/backend/task/tasks.py +++ b/backend/task/tasks.py @@ -96,7 +96,7 @@ def update_subscribed(self): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] rescan already running") - self.send_progress("Rescan already in progress.") + self.send_progress(["Rescan already in progress."]) return None manager.init(self) @@ -124,7 +124,7 @@ def download_pending(self, auto_only=False): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] download queue already running") - self.send_progress("Download Queue is already running.") + self.send_progress(["Download Queue is already running."]) return None manager.init(self) @@ -134,7 +134,7 @@ def download_pending(self, auto_only=False): if failed: print(f"[task][{self.name}] Videos failed, retry.") - self.send_progress("Videos failed, retry.") + self.send_progress(["Videos failed, retry."]) raise self.retry() except Retry as exc: @@ -176,13 +176,13 @@ def check_reindex(self, data=False, extract_videos=False): if data: # started from frontend through API print(f"[task][{self.name}] reindex {data}") - self.send_progress("Add items to the reindex Queue.") + self.send_progress(["Add items to the reindex Queue."]) ReindexManual(extract_videos=extract_videos).extract_data(data) manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] reindex queue is already running") - self.send_progress("Reindex Queue is already running.") + self.send_progress(["Reindex Queue is already running."]) return manager.init(self) @@ -190,10 +190,10 @@ def check_reindex(self, data=False, extract_videos=False): # started from scheduler populate = ReindexPopulate() print(f"[task][{self.name}] reindex outdated documents") - self.send_progress("Add recent documents to the reindex Queue.") + self.send_progress(["Add recent documents to the reindex Queue."]) populate.get_interval() populate.add_recent() - self.send_progress("Add outdated documents to the reindex Queue.") + self.send_progress(["Add outdated documents to the reindex Queue."]) populate.add_outdated() handler = Reindex(task=self) @@ -208,7 +208,7 @@ def run_manual_import(self): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] manual import is already running") - self.send_progress("Manual import is already running.") + self.send_progress(["Manual import is already running."]) return manager.init(self) @@ -221,7 +221,7 @@ def run_backup(self, reason="auto"): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] backup is already running") - self.send_progress("Backup is already running.") + self.send_progress(["Backup is already running."]) return manager.init(self) @@ -234,7 +234,7 @@ def run_restore_backup(self, filename): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] restore is already running") - self.send_progress("Restore is already running.") + self.send_progress(["Restore is already running."]) return None manager.init(self) @@ -252,7 +252,7 @@ def rescan_filesystem(self): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] filesystem rescan already running") - self.send_progress("Filesystem Rescan is already running.") + self.send_progress(["Filesystem Rescan is already running."]) return manager.init(self) @@ -268,7 +268,7 @@ def thumbnail_check(self): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] thumbnail check is already running") - self.send_progress("Thumbnail check is already running.") + self.send_progress(["Thumbnail check is already running."]) return manager.init(self) @@ -283,7 +283,7 @@ def re_sync_thumbs(self): manager = TaskManager() if manager.is_pending(self): print(f"[task][{self.name}] thumb re-embed is already running") - self.send_progress("Thumbnail re-embed is already running.") + self.send_progress(["Thumbnail re-embed is already running."]) return manager.init(self)