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/backend/appsettings/views.py b/backend/appsettings/views.py index 31978b61..edb87ca3 100644 --- a/backend/appsettings/views.py +++ b/backend/appsettings/views.py @@ -20,7 +20,7 @@ from common.serializers import ( ErrorResponseSerializer, ) from common.src.ta_redis import RedisArchivist -from common.views_base import AdminOnly, ApiBaseView +from common.views_base import AdminOnly, AdminWriteOnly, ApiBaseView from django.conf import settings from download.src.yt_dlp_base import CookieHandler, POTokenHandler from drf_spectacular.utils import OpenApiResponse, extend_schema @@ -152,7 +152,7 @@ class AppConfigApiView(ApiBaseView): POST: update app settings """ - permission_classes = [AdminOnly] + permission_classes = [AdminWriteOnly] @staticmethod @extend_schema( 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/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) diff --git a/backend/config/management/commands/ta_envcheck.py b/backend/config/management/commands/ta_envcheck.py index 6a236645..69ebd98e 100644 --- a/backend/config/management/commands/ta_envcheck.py +++ b/backend/config/management/commands/ta_envcheck.py @@ -86,6 +86,7 @@ class Command(BaseCommand): self._elastic_user_overwrite() self._ta_port_overwrite() self._ta_backend_port_overwrite() + self._disable_static_auth() self._create_superuser() def _expected_vars(self): 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" diff --git a/backend/download/serializers.py b/backend/download/serializers.py index 2ae56bb7..4ae1b180 100644 --- a/backend/download/serializers.py +++ b/backend/download/serializers.py @@ -56,7 +56,7 @@ class AddDownloadItemSerializer(serializers.Serializer): """serialize single item to add""" youtube_id = serializers.CharField() - status = serializers.ChoiceField(choices=["pending"]) + status = serializers.ChoiceField(choices=["pending", "ignore-force"]) class AddToDownloadListSerializer(serializers.Serializer): 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 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) diff --git a/backend/user/serializers.py b/backend/user/serializers.py index e8314b98..3c89d5cb 100644 --- a/backend/user/serializers.py +++ b/backend/user/serializers.py @@ -37,6 +37,7 @@ class UserMeConfigSerializer(serializers.Serializer): view_style_playlist = serializers.ChoiceField(choices=["grid", "list"]) grid_items = serializers.IntegerField(max_value=7, min_value=3) hide_watched = serializers.BooleanField() + file_size_unit = serializers.ChoiceField(choices=["binary", "metric"]) show_ignored_only = serializers.BooleanField() show_subed_only = serializers.BooleanField() show_help_text = serializers.BooleanField() diff --git a/backend/user/src/user_config.py b/backend/user/src/user_config.py index a85ce5e3..f691e980 100644 --- a/backend/user/src/user_config.py +++ b/backend/user/src/user_config.py @@ -22,6 +22,7 @@ class UserConfigType(TypedDict, total=False): view_style_playlist: str grid_items: int hide_watched: bool + file_size_unit: str show_ignored_only: bool show_subed_only: bool show_help_text: bool @@ -44,6 +45,7 @@ class UserConfig: 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, 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 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/updateDownloadQueueStatusById.ts b/frontend/src/api/actions/updateDownloadQueueStatusById.ts index 9673aab7..6b6f4eeb 100644 --- a/frontend/src/api/actions/updateDownloadQueueStatusById.ts +++ b/frontend/src/api/actions/updateDownloadQueueStatusById.ts @@ -1,6 +1,6 @@ import APIClient from '../../functions/APIClient'; -export type DownloadQueueStatus = 'ignore' | 'pending' | 'priority'; +export type DownloadQueueStatus = 'ignore' | 'ignore-force' | 'pending' | 'priority'; const updateDownloadQueueStatusById = async (youtubeId: string, status: DownloadQueueStatus) => { return APIClient(`/api/download/${youtubeId}/`, { diff --git a/frontend/src/api/actions/updateUserConfig.ts b/frontend/src/api/actions/updateUserConfig.ts index 675d80d9..06eeddab 100644 --- a/frontend/src/api/actions/updateUserConfig.ts +++ b/frontend/src/api/actions/updateUserConfig.ts @@ -3,6 +3,11 @@ import APIClient from '../../functions/APIClient'; export type ColourVariants = 'dark.css' | 'light.css' | 'matrix.css' | 'midnight.css'; +export const FileSizeUnits = { + Binary: 'binary', + Metric: 'metric', +}; + export type UserConfigType = { stylesheet: ColourVariants; page_size: number; @@ -14,13 +19,14 @@ export type UserConfigType = { view_style_playlist: ViewLayoutType; grid_items: number; hide_watched: boolean; + file_size_unit: 'binary' | 'metric'; show_ignored_only: boolean; show_subed_only: boolean; 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 6358ede5..df8ca978 100644 --- a/frontend/src/components/BiggestChannelsStats.tsx +++ b/frontend/src/components/BiggestChannelsStats.tsx @@ -2,20 +2,20 @@ 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; biggestChannelsStatsByDuration?: BiggestChannelsStatsType; biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType; - useSI: boolean; + useSIUnits: boolean; }; const BiggestChannelsStats = ({ biggestChannelsStatsByCount, biggestChannelsStatsByDuration, biggestChannelsStatsByMediaSize, - useSI, + useSIUnits, }: BiggestChannelsStatsProps) => { if ( !biggestChannelsStatsByCount && @@ -94,7 +94,9 @@ const BiggestChannelsStats = ({ {name} - {humanFileSize(media_size, useSI)} + + {humanFileSize(media_size, useSIUnits)} + ); })} diff --git a/frontend/src/components/DownloadHistoryStats.tsx b/frontend/src/components/DownloadHistoryStats.tsx index 935fa314..fdc90fb0 100644 --- a/frontend/src/components/DownloadHistoryStats.tsx +++ b/frontend/src/components/DownloadHistoryStats.tsx @@ -1,14 +1,14 @@ 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; - useSI: boolean; + useSIUnits: boolean; }; -const DownloadHistoryStats = ({ downloadHistoryStats, useSI }: DownloadHistoryStatsProps) => { +const DownloadHistoryStats = ({ downloadHistoryStats, useSIUnits }: DownloadHistoryStatsProps) => { if (!downloadHistoryStats) { return

Loading...

; } @@ -31,7 +31,7 @@ const DownloadHistoryStats = ({ downloadHistoryStats, useSI }: DownloadHistorySt

+{formatNumbers(count)} {videoText}
- {humanFileSize(media_size, useSI)} + {humanFileSize(media_size, useSIUnits)}

); 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 => (
{ +const OverviewStats = ({ videoStats, useSIUnits }: OverviewStatsProps) => { if (!videoStats) { return

Loading...

; } @@ -19,7 +19,7 @@ const OverviewStats = ({ videoStats, useSI }: OverviewStatsProps) => { title: 'All: ', data: { Videos: formatNumbers(videoStats?.doc_count || 0), - ['Media Size']: humanFileSize(videoStats?.media_size || 0, useSI), + ['Media Size']: humanFileSize(videoStats?.media_size || 0, useSIUnits), Duration: videoStats?.duration_str, }, }, @@ -27,7 +27,7 @@ const OverviewStats = ({ videoStats, useSI }: OverviewStatsProps) => { title: 'Active: ', data: { Videos: formatNumbers(videoStats?.active_true?.doc_count || 0), - ['Media Size']: humanFileSize(videoStats?.active_true?.media_size || 0, useSI), + ['Media Size']: humanFileSize(videoStats?.active_true?.media_size || 0, useSIUnits), Duration: videoStats?.active_true?.duration_str || 'NA', }, }, @@ -35,7 +35,7 @@ const OverviewStats = ({ videoStats, useSI }: OverviewStatsProps) => { title: 'Inactive: ', data: { Videos: formatNumbers(videoStats?.active_false?.doc_count || 0), - ['Media Size']: humanFileSize(videoStats?.active_false?.media_size || 0, useSI), + ['Media Size']: humanFileSize(videoStats?.active_false?.media_size || 0, useSIUnits), Duration: videoStats?.active_false?.duration_str || 'NA', }, }, diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx index a249c1c5..4778ba4b 100644 --- a/frontend/src/components/VideoPlayer.tsx +++ b/frontend/src/components/VideoPlayer.tsx @@ -1,5 +1,5 @@ import updateVideoProgressById from '../api/actions/updateVideoProgressById'; -import { SponsorBlockSegmentType, SponsorBlockType, VideoResponseType } from '../pages/Video'; +import { SponsorBlockSegmentType, SponsorBlockType } from '../pages/Video'; import { Dispatch, Fragment, @@ -13,6 +13,7 @@ import formatTime from '../functions/formatTime'; import { useSearchParams } from 'react-router-dom'; import getApiUrl from '../configuration/getApiUrl'; import { useKeyPress } from '../functions/useKeypressHook'; +import { VideoResponseType } from '../api/loader/loadVideoById'; const VIDEO_PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3]; @@ -95,7 +96,9 @@ const handleTimeUpdate = currentProgress: currentTime, }); - if (videoProgressResponse.watched && watched !== videoProgressResponse.watched) { + const { data: videoProgressResponseData } = videoProgressResponse ?? {}; + + if (videoProgressResponseData?.watched && watched !== videoProgressResponseData.watched) { onWatchStateChanged?.(true); } } @@ -185,7 +188,9 @@ const VideoPlayer = ({ currentProgress: currentTime, }); - if (videoProgressResponse.watched && watched !== videoProgressResponse.watched) { + const { data: videoProgressResponseData } = videoProgressResponse; + + if (videoProgressResponseData?.watched && watched !== videoProgressResponseData.watched) { onWatchStateChanged?.(true); } diff --git a/frontend/src/components/VideoTypeStats.tsx b/frontend/src/components/VideoTypeStats.tsx index 1aacfecf..5c48939b 100644 --- a/frontend/src/components/VideoTypeStats.tsx +++ b/frontend/src/components/VideoTypeStats.tsx @@ -2,14 +2,14 @@ import { Fragment } from 'react'; import humanFileSize from '../functions/humanFileSize'; import StatsInfoBoxItem from './StatsInfoBoxItem'; import formatNumbers from '../functions/formatNumbers'; -import { VideoStatsType } from '../pages/SettingsDashboard'; +import { VideoStatsType } from '../api/loader/loadStatsVideo'; type VideoTypeStatsProps = { videoStats?: VideoStatsType; - useSI: boolean; + useSIUnits: boolean; }; -const VideoTypeStats = ({ videoStats, useSI }: VideoTypeStatsProps) => { +const VideoTypeStats = ({ videoStats, useSIUnits }: VideoTypeStatsProps) => { if (!videoStats) { return

Loading...

; } @@ -19,7 +19,7 @@ const VideoTypeStats = ({ videoStats, useSI }: VideoTypeStatsProps) => { title: 'Regular Videos: ', data: { Videos: formatNumbers(videoStats?.type_videos?.doc_count || 0), - ['Media Size']: humanFileSize(videoStats?.type_videos?.media_size || 0, useSI), + ['Media Size']: humanFileSize(videoStats?.type_videos?.media_size || 0, useSIUnits), Duration: videoStats?.type_videos?.duration_str || 'NA', }, }, @@ -27,7 +27,7 @@ const VideoTypeStats = ({ videoStats, useSI }: VideoTypeStatsProps) => { title: 'Shorts: ', data: { Videos: formatNumbers(videoStats?.type_shorts?.doc_count || 0), - ['Media Size']: humanFileSize(videoStats?.type_shorts?.media_size || 0, useSI), + ['Media Size']: humanFileSize(videoStats?.type_shorts?.media_size || 0, useSIUnits), Duration: videoStats?.type_shorts?.duration_str || 'NA', }, }, @@ -35,7 +35,7 @@ const VideoTypeStats = ({ videoStats, useSI }: VideoTypeStatsProps) => { title: 'Streams: ', data: { Videos: formatNumbers(videoStats?.type_streams?.doc_count || 0), - ['Media Size']: humanFileSize(videoStats?.type_streams?.media_size || 0, useSI), + ['Media Size']: humanFileSize(videoStats?.type_streams?.media_size || 0, useSIUnits), Duration: videoStats?.type_streams?.duration_str || 'NA', }, }, diff --git a/frontend/src/components/WatchProgressStats.tsx b/frontend/src/components/WatchProgressStats.tsx index afa43752..8524cb71 100644 --- a/frontend/src/components/WatchProgressStats.tsx +++ b/frontend/src/components/WatchProgressStats.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react'; import StatsInfoBoxItem from './StatsInfoBoxItem'; import formatNumbers from '../functions/formatNumbers'; -import { WatchProgressStatsType } from '../pages/SettingsDashboard'; +import { WatchProgressStatsType } from '../api/loader/loadStatsWatchProgress'; const formatProgress = (progress: number) => { return (Number(progress) * 100).toFixed(2) ?? '0'; diff --git a/frontend/src/functions/APIClient.ts b/frontend/src/functions/APIClient.ts index c01ba8b8..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,27 +65,37 @@ const APIClient = async ( throw new Error('Forbidden: Access denied.'); } - 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 4fc91d7a..53868ba0 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/NotFound'; const router = createBrowserRouter( [ @@ -50,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 }; }, @@ -145,6 +146,11 @@ const router = createBrowserRouter( element: , errorElement: , }, + { + 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 2da7c552..05bf21af 100644 --- a/frontend/src/pages/ChannelBase.tsx +++ b/frontend/src/pages/ChannelBase.tsx @@ -1,42 +1,50 @@ 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 { 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 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 () => { - const channelNavResponse = await loadChannelNav(channelId); const channelResponse = await loadChannelById(channelId); - setChannelResponse(channelResponse); + + const channelNavResponse = await loadChannelNav(channelId); setChannelNav(channelNavResponse); })(); }, [channelId]); + 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 52dbdcda..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(() => { @@ -37,6 +44,7 @@ const ChannelPlaylist = () => { const playlists = await loadPlaylistList({ channel: channelId, subscribed: showSubedOnly, + page: currentPage, }); setPlaylistsResponse(playlists); diff --git a/frontend/src/pages/ChannelVideo.tsx b/frontend/src/pages/ChannelVideo.tsx index 2533bebe..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'; @@ -20,6 +19,8 @@ import loadVideoListByFilter, { 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; @@ -38,15 +39,22 @@ 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; const isGridView = view === ViewStyles.grid; @@ -107,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, true)} + {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..b32c276b 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/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 +133,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 18dd241b..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'; @@ -40,6 +38,11 @@ import useIsAdmin from '../functions/useIsAdmin'; 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'; +import NotFound from './NotFound'; +import { ApiResponseType } from '../functions/APIClient'; const isInPlaylist = (videoId: string, playlist: PlaylistType) => { return playlist.playlist_entries.some(entry => { @@ -76,7 +79,7 @@ type PlaylistNavItemType = { playlist_next: PlaylistNavNextItemType; }; -type PlaylistNavType = PlaylistNavItemType[]; +export type PlaylistNavType = PlaylistNavItemType[]; export type SponsorBlockSegmentType = { category: string; @@ -96,21 +99,12 @@ 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(); const isAdmin = useIsAdmin(); const { appSettingsConfig } = useAppSettingsStore(); + const { userConfig } = useUserConfigStore(); const [videoEnded, setVideoEnded] = useState(false); const [playlistAutoplay, setPlaylistAutoplay] = useState( @@ -125,17 +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) { + if (refreshVideoList || videoId !== videoResponseData?.youtube_id) { const videoByIdResponse = await loadVideoById(videoId); - const simmilarVideosResponse = await loadSimmilarVideosById(videoId); + setVideoResponse(videoByIdResponse); + + const similarVideosResponse = await loadSimilarVideosById(videoId); const customPlaylistsResponse = await loadPlaylistList({ type: 'custom' }); const videoNavResponse = await loadVideoNav(videoId); @@ -146,8 +150,7 @@ const Video = () => { console.log('Comments not found', e); } - setVideoResponse(videoByIdResponse); - setSimmilarVideos(simmilarVideosResponse); + setSimilarVideos(similarVideosResponse); setVideoPlaylistNav(videoNavResponse); setCustomPlaylistsResponse(customPlaylistsResponse); @@ -171,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; }); @@ -187,17 +190,24 @@ const Video = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [videoEnded, playlistAutoplay]); - if (videoResponse === undefined) { + const errorMessage = videoResponseError?.error; + + 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); @@ -209,7 +219,7 @@ const Video = () => { { @@ -358,7 +368,15 @@ const Video = () => { navigate(Routes.Channel(video.channel.channel_id)); }} /> - +
- {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} @@ -565,7 +583,7 @@ const Video = () => {

Similar Videos

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, diff --git a/frontend/src/style.css b/frontend/src/style.css index b212c000..f5301d5b 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -179,6 +179,10 @@ button:hover { padding: 5px 0 5px 1rem; } +.left-align { + text-align: start; +} + .help-text::before { content: '?'; font-size: 1.5em; 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, + }, });