mirror of
https://github.com/tubearchivist/tubearchivist.git
synced 2025-04-20 03:40:12 +00:00
Various fixes, #build
Changed: - Added better debug statements to login page - Fixed appsettings admin read permissions - Fixed notification types - Fixed DISABLE_STATIC_AUTH
This commit is contained in:
commit
4ae59848b4
@ -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 |
|
||||
|
@ -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(
|
||||
|
@ -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)
|
@ -25,4 +25,9 @@ urlpatterns = [
|
||||
views.NotificationView.as_view(),
|
||||
name="api-notification",
|
||||
),
|
||||
path(
|
||||
"health/",
|
||||
views.HealthCheck.as_view(),
|
||||
name="api-health",
|
||||
),
|
||||
]
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -1,8 +1,8 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { CookieStateType } from '../loader/loadCookie';
|
||||
|
||||
const deleteCookie = async (): Promise<CookieStateType> => {
|
||||
return APIClient('/api/appsettings/cookie/', {
|
||||
const deleteCookie = async () => {
|
||||
return APIClient<CookieStateType>('/api/appsettings/cookie/', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { CookieStateType } from '../loader/loadCookie';
|
||||
|
||||
const updateCookie = async (cookie: string): Promise<CookieStateType> => {
|
||||
return APIClient('/api/appsettings/cookie/', {
|
||||
const updateCookie = async (cookie: string) => {
|
||||
return APIClient<CookieStateType>('/api/appsettings/cookie/', {
|
||||
method: 'PUT',
|
||||
body: { cookie },
|
||||
});
|
||||
|
@ -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}/`, {
|
||||
|
@ -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<UserConfigType>): Promise<UserConfigType> => {
|
||||
return APIClient('/api/user/me/', {
|
||||
const updateUserConfig = async (config: Partial<UserConfigType>) => {
|
||||
return APIClient<UserConfigType>('/api/user/me/', {
|
||||
method: 'POST',
|
||||
body: config,
|
||||
});
|
||||
|
@ -14,11 +14,8 @@ type VideoProgressProp = {
|
||||
currentProgress: number;
|
||||
};
|
||||
|
||||
const updateVideoProgressById = async ({
|
||||
youtubeId,
|
||||
currentProgress,
|
||||
}: VideoProgressProp): Promise<VideoProgressResponseType> => {
|
||||
return APIClient(`/api/video/${youtubeId}/progress/`, {
|
||||
const updateVideoProgressById = async ({ youtubeId, currentProgress }: VideoProgressProp) => {
|
||||
return APIClient<VideoProgressResponseType>(`/api/video/${youtubeId}/progress/`, {
|
||||
method: 'POST',
|
||||
body: { position: currentProgress },
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { CookieStateType } from '../loader/loadCookie';
|
||||
|
||||
const validateCookie = async (): Promise<CookieStateType> => {
|
||||
return APIClient('/api/appsettings/cookie/', {
|
||||
const validateCookie = async () => {
|
||||
return APIClient<CookieStateType>('/api/appsettings/cookie/', {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
@ -4,8 +4,8 @@ type ApiTokenResponse = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
const loadApiToken = async (): Promise<ApiTokenResponse> => {
|
||||
return APIClient('/api/appsettings/token/');
|
||||
const loadApiToken = async () => {
|
||||
return APIClient<ApiTokenResponse>('/api/appsettings/token/');
|
||||
};
|
||||
|
||||
export default loadApiToken;
|
||||
|
@ -19,8 +19,8 @@ export type AppriseNotificationType = {
|
||||
};
|
||||
};
|
||||
|
||||
const loadAppriseNotification = async (): Promise<AppriseNotificationType> => {
|
||||
return APIClient('/api/task/notification/');
|
||||
const loadAppriseNotification = async () => {
|
||||
return APIClient<AppriseNotificationType>('/api/task/notification/');
|
||||
};
|
||||
|
||||
export default loadAppriseNotification;
|
||||
|
@ -33,8 +33,8 @@ export type AppSettingsConfigType = {
|
||||
};
|
||||
};
|
||||
|
||||
const loadAppsettingsConfig = async (): Promise<AppSettingsConfigType> => {
|
||||
return APIClient('/api/appsettings/config/');
|
||||
const loadAppsettingsConfig = async () => {
|
||||
return APIClient<AppSettingsConfigType>('/api/appsettings/config/');
|
||||
};
|
||||
|
||||
export default loadAppsettingsConfig;
|
||||
|
@ -1,7 +1,17 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
type Backup = {
|
||||
filename: string;
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
timestamp: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type BackupListType = Backup[];
|
||||
|
||||
const loadBackupList = async () => {
|
||||
return APIClient('/api/appsettings/backup/');
|
||||
return APIClient<BackupListType>('/api/appsettings/backup/');
|
||||
};
|
||||
|
||||
export default loadBackupList;
|
||||
|
@ -13,8 +13,8 @@ export type ChannelAggsType = {
|
||||
};
|
||||
};
|
||||
|
||||
const loadChannelAggs = async (channelId: string): Promise<ChannelAggsType> => {
|
||||
return APIClient(`/api/channel/${channelId}/aggs/`);
|
||||
const loadChannelAggs = async (channelId: string) => {
|
||||
return APIClient<ChannelAggsType>(`/api/channel/${channelId}/aggs/`);
|
||||
};
|
||||
|
||||
export default loadChannelAggs;
|
||||
|
@ -1,8 +1,10 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { ChannelResponseType } from '../../pages/ChannelBase';
|
||||
import { ChannelType } from '../../pages/Channels';
|
||||
|
||||
const loadChannelById = async (youtubeChannelId: string): Promise<ChannelResponseType> => {
|
||||
return APIClient(`/api/channel/${youtubeChannelId}/`);
|
||||
export type ChannelResponseType = ChannelType;
|
||||
|
||||
const loadChannelById = async (youtubeChannelId: string) => {
|
||||
return APIClient<ChannelResponseType>(`/api/channel/${youtubeChannelId}/`);
|
||||
};
|
||||
|
||||
export default loadChannelById;
|
||||
|
@ -1,4 +1,13 @@
|
||||
import { PaginationType } from '../../components/Pagination';
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { ChannelType } from '../../pages/Channels';
|
||||
import { ConfigType } from '../../pages/Home';
|
||||
|
||||
export type ChannelsListResponse = {
|
||||
data: ChannelType[];
|
||||
paginate: PaginationType;
|
||||
config?: ConfigType;
|
||||
};
|
||||
|
||||
const loadChannelList = async (page: number, showSubscribed: boolean) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
@ -8,7 +17,7 @@ const loadChannelList = async (page: number, showSubscribed: boolean) => {
|
||||
|
||||
const endpoint = `/api/channel/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
|
||||
return APIClient(endpoint);
|
||||
return APIClient<ChannelsListResponse>(endpoint);
|
||||
};
|
||||
|
||||
export default loadChannelList;
|
||||
|
@ -7,8 +7,8 @@ export type ChannelNavResponseType = {
|
||||
has_pending: boolean;
|
||||
};
|
||||
|
||||
const loadChannelNav = async (youtubeChannelId: string): Promise<ChannelNavResponseType> => {
|
||||
return APIClient(`/api/channel/${youtubeChannelId}/nav/`);
|
||||
const loadChannelNav = async (youtubeChannelId: string) => {
|
||||
return APIClient<ChannelNavResponseType>(`/api/channel/${youtubeChannelId}/nav/`);
|
||||
};
|
||||
|
||||
export default loadChannelNav;
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { CommentsType } from '../../components/CommentBox';
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type CommentsResponseType = CommentsType[];
|
||||
|
||||
const loadCommentsbyVideoId = async (youtubeId: string) => {
|
||||
return APIClient(`/api/video/${youtubeId}/comment/`);
|
||||
return APIClient<CommentsResponseType>(`/api/video/${youtubeId}/comment/`);
|
||||
};
|
||||
|
||||
export default loadCommentsbyVideoId;
|
||||
|
@ -7,8 +7,8 @@ export type CookieStateType = {
|
||||
validated_str?: string;
|
||||
};
|
||||
|
||||
const loadCookie = async (): Promise<CookieStateType> => {
|
||||
return APIClient('/api/appsettings/cookie/');
|
||||
const loadCookie = async () => {
|
||||
return APIClient<CookieStateType>('/api/appsettings/cookie/');
|
||||
};
|
||||
|
||||
export default loadCookie;
|
||||
|
@ -12,10 +12,10 @@ export type DownloadAggsType = {
|
||||
buckets: DownloadAggsBucket[];
|
||||
};
|
||||
|
||||
const loadDownloadAggs = async (showIgnored: boolean): Promise<DownloadAggsType> => {
|
||||
const loadDownloadAggs = async (showIgnored: boolean) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('filter', showIgnored ? 'ignore' : 'pending');
|
||||
return APIClient(
|
||||
return APIClient<DownloadAggsType>(
|
||||
`/api/download/aggs/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`,
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,7 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { DownloadResponseType } from '../../pages/Download';
|
||||
|
||||
const loadDownloadQueue = async (
|
||||
page: number,
|
||||
channelId: string | null,
|
||||
showIgnored: boolean,
|
||||
): Promise<DownloadResponseType> => {
|
||||
const loadDownloadQueue = async (page: number, channelId: string | null, showIgnored: boolean) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (page) searchParams.append('page', page.toString());
|
||||
@ -14,7 +10,7 @@ const loadDownloadQueue = async (
|
||||
|
||||
const endpoint = `/api/download/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
|
||||
return APIClient(endpoint);
|
||||
return APIClient<DownloadResponseType>(endpoint);
|
||||
};
|
||||
|
||||
export default loadDownloadQueue;
|
||||
|
@ -2,6 +2,19 @@ import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type NotificationPages = 'download' | 'settings' | 'channel' | 'all';
|
||||
|
||||
type NotificationType = {
|
||||
title: string;
|
||||
group: string;
|
||||
api_stop: boolean;
|
||||
level: string;
|
||||
id: string;
|
||||
command: boolean | string;
|
||||
messages: string[];
|
||||
progress: number;
|
||||
};
|
||||
|
||||
export type NotificationResponseType = NotificationType[];
|
||||
|
||||
const loadNotifications = async (pageName: NotificationPages, includeReindex = false) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
@ -10,7 +23,7 @@ const loadNotifications = async (pageName: NotificationPages, includeReindex = f
|
||||
}
|
||||
|
||||
const endpoint = `/api/notification/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
return APIClient(endpoint);
|
||||
return APIClient<NotificationResponseType>(endpoint);
|
||||
};
|
||||
|
||||
export default loadNotifications;
|
||||
|
@ -26,8 +26,8 @@ export type PlaylistType = {
|
||||
|
||||
export type PlaylistResponseType = PlaylistType;
|
||||
|
||||
const loadPlaylistById = async (playlistId: string | undefined): Promise<PlaylistResponseType> => {
|
||||
return APIClient(`/api/playlist/${playlistId}/`);
|
||||
const loadPlaylistById = async (playlistId: string | undefined) => {
|
||||
return APIClient<PlaylistResponseType>(`/api/playlist/${playlistId}/`);
|
||||
};
|
||||
|
||||
export default loadPlaylistById;
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { PaginationType } from '../../components/Pagination';
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { PlaylistType } from './loadPlaylistById';
|
||||
|
||||
type PlaylistType = 'regular' | 'custom';
|
||||
export type PlaylistsResponseType = {
|
||||
data?: PlaylistType[];
|
||||
paginate?: PaginationType;
|
||||
};
|
||||
|
||||
type PlaylistCategoryType = 'regular' | 'custom';
|
||||
|
||||
type LoadPlaylistListProps = {
|
||||
channel?: string;
|
||||
page?: number | undefined;
|
||||
subscribed?: boolean;
|
||||
type?: PlaylistType;
|
||||
type?: PlaylistCategoryType;
|
||||
};
|
||||
|
||||
const loadPlaylistList = async ({ channel, page, subscribed, type }: LoadPlaylistListProps) => {
|
||||
@ -18,7 +25,7 @@ const loadPlaylistList = async ({ channel, page, subscribed, type }: LoadPlaylis
|
||||
if (type) searchParams.append('type', type);
|
||||
|
||||
const endpoint = `/api/playlist/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
return APIClient(endpoint);
|
||||
return APIClient<PlaylistsResponseType>(endpoint);
|
||||
};
|
||||
|
||||
export default loadPlaylistList;
|
||||
|
@ -13,8 +13,8 @@ type ScheduleType = {
|
||||
|
||||
export type ScheduleResponseType = ScheduleType[];
|
||||
|
||||
const loadSchedule = async (): Promise<ScheduleResponseType> => {
|
||||
return APIClient('/api/task/schedule/');
|
||||
const loadSchedule = async () => {
|
||||
return APIClient<ScheduleResponseType>('/api/task/schedule/');
|
||||
};
|
||||
|
||||
export default loadSchedule;
|
||||
|
@ -1,7 +1,22 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { ChannelType } from '../../pages/Channels';
|
||||
import { VideoType } from '../../pages/Home';
|
||||
import { PlaylistType } from './loadPlaylistById';
|
||||
|
||||
type SearchResultType = {
|
||||
video_results: VideoType[];
|
||||
channel_results: ChannelType[];
|
||||
playlist_results: PlaylistType[];
|
||||
fulltext_results: [];
|
||||
};
|
||||
|
||||
export type SearchResultsType = {
|
||||
results: SearchResultType;
|
||||
queryType: string;
|
||||
};
|
||||
|
||||
const loadSearch = async (query: string) => {
|
||||
return APIClient(`/api/search/?query=${query}`);
|
||||
return APIClient<SearchResultsType>(`/api/search/?query=${query}`);
|
||||
};
|
||||
|
||||
export default loadSearch;
|
||||
|
8
frontend/src/api/loader/loadSimilarVideosById.ts
Normal file
8
frontend/src/api/loader/loadSimilarVideosById.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { VideoResponseType } from './loadVideoById';
|
||||
|
||||
const loadSimilarVideosById = async (youtubeId: string) => {
|
||||
return APIClient<VideoResponseType[]>(`/api/video/${youtubeId}/similar/`);
|
||||
};
|
||||
|
||||
export default loadSimilarVideosById;
|
@ -1,7 +0,0 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
const loadSimmilarVideosById = async (youtubeId: string) => {
|
||||
return APIClient(`/api/video/${youtubeId}/similar/`);
|
||||
};
|
||||
|
||||
export default loadSimmilarVideosById;
|
@ -1,7 +1,24 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type SnapshotType = {
|
||||
id: string;
|
||||
state: string;
|
||||
es_version: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
end_stamp: number;
|
||||
duration_s: number;
|
||||
};
|
||||
|
||||
export type SnapshotListType = {
|
||||
next_exec: number;
|
||||
next_exec_str: string;
|
||||
expire_after: string;
|
||||
snapshots?: SnapshotType[];
|
||||
};
|
||||
|
||||
const loadSnapshots = async () => {
|
||||
return APIClient('/api/appsettings/snapshot/');
|
||||
return APIClient<SnapshotListType>('/api/appsettings/snapshot/');
|
||||
};
|
||||
|
||||
export default loadSnapshots;
|
||||
|
@ -2,11 +2,24 @@ import APIClient from '../../functions/APIClient';
|
||||
|
||||
type BiggestChannelsOrderType = 'doc_count' | 'duration' | 'media_size';
|
||||
|
||||
type BiggestChannelsType = {
|
||||
id: string;
|
||||
name: string;
|
||||
doc_count: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
media_size: number;
|
||||
};
|
||||
|
||||
export type BiggestChannelsStatsType = BiggestChannelsType[];
|
||||
|
||||
const loadStatsBiggestChannels = async (order: BiggestChannelsOrderType) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('order', order);
|
||||
|
||||
return APIClient(`/api/stats/biggestchannels/?${searchParams.toString()}`);
|
||||
return APIClient<BiggestChannelsStatsType>(
|
||||
`/api/stats/biggestchannels/?${searchParams.toString()}`,
|
||||
);
|
||||
};
|
||||
|
||||
export default loadStatsBiggestChannels;
|
||||
|
@ -1,7 +1,13 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type ChannelStatsType = {
|
||||
doc_count: number;
|
||||
active_true: number;
|
||||
subscribed_true: number;
|
||||
};
|
||||
|
||||
const loadStatsChannel = async () => {
|
||||
return APIClient('/api/stats/channel/');
|
||||
return APIClient<ChannelStatsType>('/api/stats/channel/');
|
||||
};
|
||||
|
||||
export default loadStatsChannel;
|
||||
|
@ -1,7 +1,14 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type DownloadStatsType = {
|
||||
pending: number;
|
||||
pending_videos: number;
|
||||
pending_shorts: number;
|
||||
pending_streams: number;
|
||||
};
|
||||
|
||||
const loadStatsDownload = async () => {
|
||||
return APIClient('/api/stats/download/');
|
||||
return APIClient<DownloadStatsType>('/api/stats/download/');
|
||||
};
|
||||
|
||||
export default loadStatsDownload;
|
||||
|
@ -1,7 +1,15 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
type DownloadHistoryType = {
|
||||
date: string;
|
||||
count: number;
|
||||
media_size: number;
|
||||
};
|
||||
|
||||
export type DownloadHistoryStatsType = DownloadHistoryType[];
|
||||
|
||||
const loadStatsDownloadHistory = async () => {
|
||||
return APIClient('/api/stats/downloadhist/');
|
||||
return APIClient<DownloadHistoryStatsType>('/api/stats/downloadhist/');
|
||||
};
|
||||
|
||||
export default loadStatsDownloadHistory;
|
||||
|
@ -1,7 +1,14 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type PlaylistStatsType = {
|
||||
doc_count: number;
|
||||
active_false: number;
|
||||
active_true: number;
|
||||
subscribed_true: number;
|
||||
};
|
||||
|
||||
const loadStatsPlaylist = async () => {
|
||||
return APIClient('/api/stats/playlist/');
|
||||
return APIClient<PlaylistStatsType>('/api/stats/playlist/');
|
||||
};
|
||||
|
||||
export default loadStatsPlaylist;
|
||||
|
@ -1,7 +1,44 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type VideoStatsType = {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
type_videos: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
type_shorts: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
active_true: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
active_false: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
type_streams: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
};
|
||||
|
||||
const loadStatsVideo = async () => {
|
||||
return APIClient('/api/stats/video/');
|
||||
return APIClient<VideoStatsType>('/api/stats/video/');
|
||||
};
|
||||
|
||||
export default loadStatsVideo;
|
||||
|
@ -1,7 +1,27 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
export type WatchProgressStatsType = {
|
||||
total: {
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
items: number;
|
||||
};
|
||||
unwatched: {
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
progress: number;
|
||||
items: number;
|
||||
};
|
||||
watched: {
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
progress: number;
|
||||
items: number;
|
||||
};
|
||||
};
|
||||
|
||||
const loadStatsWatchProgress = async () => {
|
||||
return APIClient('/api/stats/watch/');
|
||||
return APIClient<WatchProgressStatsType>('/api/stats/watch/');
|
||||
};
|
||||
|
||||
export default loadStatsWatchProgress;
|
||||
|
@ -10,8 +10,8 @@ export type UserAccountType = {
|
||||
last_login: string;
|
||||
};
|
||||
|
||||
const loadUserAccount = async (): Promise<UserAccountType> => {
|
||||
return APIClient('/api/user/account/');
|
||||
const loadUserAccount = async () => {
|
||||
return APIClient<UserAccountType>('/api/user/account/');
|
||||
};
|
||||
|
||||
export default loadUserAccount;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { UserConfigType } from '../actions/updateUserConfig';
|
||||
import APIClient from '../../functions/APIClient';
|
||||
|
||||
const loadUserMeConfig = async (): Promise<UserConfigType> => {
|
||||
return APIClient('/api/user/me/');
|
||||
const loadUserMeConfig = async () => {
|
||||
return APIClient<UserConfigType>('/api/user/me/');
|
||||
};
|
||||
|
||||
export default loadUserMeConfig;
|
||||
|
@ -1,8 +1,10 @@
|
||||
import APIClient from '../../functions/APIClient';
|
||||
import { VideoResponseType } from '../../pages/Video';
|
||||
import { VideoType } from '../../pages/Home';
|
||||
|
||||
const loadVideoById = async (youtubeId: string): Promise<VideoResponseType> => {
|
||||
return APIClient(`/api/video/${youtubeId}/`);
|
||||
export type VideoResponseType = VideoType;
|
||||
|
||||
const loadVideoById = async (youtubeId: string) => {
|
||||
return APIClient<VideoResponseType>(`/api/video/${youtubeId}/`);
|
||||
};
|
||||
|
||||
export default loadVideoById;
|
||||
|
@ -21,9 +21,7 @@ type FilterType = {
|
||||
type?: VideoTypes;
|
||||
};
|
||||
|
||||
const loadVideoListByFilter = async (
|
||||
filter: FilterType,
|
||||
): Promise<VideoListByFilterResponseType> => {
|
||||
const loadVideoListByFilter = async (filter: FilterType) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (filter.playlist) {
|
||||
@ -39,7 +37,7 @@ const loadVideoListByFilter = async (
|
||||
if (filter.type) searchParams.append('type', filter.type);
|
||||
|
||||
const endpoint = `/api/video/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
|
||||
return APIClient(endpoint);
|
||||
return APIClient<VideoListByFilterResponseType>(endpoint);
|
||||
};
|
||||
|
||||
export default loadVideoListByFilter;
|
||||
|
@ -25,8 +25,8 @@ export type VideoNavResponseType = {
|
||||
};
|
||||
};
|
||||
|
||||
const loadVideoNav = async (youtubeVideoId: string): Promise<VideoNavResponseType[]> => {
|
||||
return APIClient(`/api/video/${youtubeVideoId}/nav/`);
|
||||
const loadVideoNav = async (youtubeVideoId: string) => {
|
||||
return APIClient<VideoNavResponseType[]>(`/api/video/${youtubeVideoId}/nav/`);
|
||||
};
|
||||
|
||||
export default loadVideoNav;
|
||||
|
@ -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;
|
||||
|
@ -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 = ({
|
||||
<td className="agg-channel-name">
|
||||
<Link to={Routes.Channel(id)}>{name}</Link>
|
||||
</td>
|
||||
<td className="agg-channel-right-align">{humanFileSize(media_size, useSI)}</td>
|
||||
<td className="agg-channel-right-align">
|
||||
{humanFileSize(media_size, useSIUnits)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
@ -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 <p id="loading">Loading...</p>;
|
||||
}
|
||||
@ -31,7 +31,7 @@ const DownloadHistoryStats = ({ downloadHistoryStats, useSI }: DownloadHistorySt
|
||||
<p>
|
||||
+{formatNumbers(count)} {videoText}
|
||||
<br />
|
||||
{humanFileSize(media_size, useSI)}
|
||||
{humanFileSize(media_size, useSIUnits)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { VideoResponseType } from '../pages/Video';
|
||||
import VideoPlayer from './VideoPlayer';
|
||||
import loadVideoById from '../api/loader/loadVideoById';
|
||||
import loadVideoById, { VideoResponseType } from '../api/loader/loadVideoById';
|
||||
import iconClose from '/img/icon-close.svg';
|
||||
import iconEye from '/img/icon-eye.svg';
|
||||
import iconThumb from '/img/icon-thumb.svg';
|
||||
@ -13,6 +12,7 @@ import { Link, useSearchParams } from 'react-router-dom';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
import loadPlaylistById from '../api/loader/loadPlaylistById';
|
||||
import { useAppSettingsStore } from '../stores/AppSettingsStore';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
type Playlist = {
|
||||
id: string;
|
||||
@ -32,9 +32,11 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
|
||||
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
|
||||
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
|
||||
const [playlists, setPlaylists] = useState<PlaylistList>();
|
||||
|
||||
const { data: videoResponseData } = videoResponse ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!videoId) {
|
||||
@ -43,27 +45,31 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
|
||||
|
||||
inlinePlayerRef.current?.scrollIntoView();
|
||||
|
||||
if (refresh || videoId !== videoResponse?.youtube_id) {
|
||||
if (refresh || videoId !== videoResponseData?.youtube_id) {
|
||||
const videoResponse = await loadVideoById(videoId);
|
||||
|
||||
const playlistIds = videoResponse.playlist;
|
||||
const { data: videoResponseData } = videoResponse ?? {};
|
||||
|
||||
const playlistIds = videoResponseData?.playlist || [];
|
||||
if (playlistIds !== undefined) {
|
||||
const playlists = await Promise.all(
|
||||
playlistIds.map(async playlistid => {
|
||||
const playlistResponse = await loadPlaylistById(playlistid);
|
||||
|
||||
return playlistResponse;
|
||||
const { data: playlistResponseData } = playlistResponse ?? {};
|
||||
|
||||
return playlistResponseData;
|
||||
}),
|
||||
);
|
||||
|
||||
const playlistsFiltered = playlists
|
||||
.filter(playlist => {
|
||||
return playlist.playlist_subscribed;
|
||||
return playlist?.playlist_subscribed;
|
||||
})
|
||||
.map(playlist => {
|
||||
return {
|
||||
id: playlist.playlist_id,
|
||||
name: playlist.playlist_name,
|
||||
id: playlist?.playlist_id || '',
|
||||
name: playlist?.playlist_name || '',
|
||||
};
|
||||
});
|
||||
|
||||
@ -77,11 +83,11 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [videoId, refresh]);
|
||||
|
||||
if (videoResponse === undefined || videoId === null) {
|
||||
if (videoResponseData === undefined || videoId === null) {
|
||||
return <div ref={inlinePlayerRef} className="player-wrapper" />;
|
||||
}
|
||||
|
||||
const video = videoResponse;
|
||||
const video = videoResponseData;
|
||||
const name = video.title;
|
||||
const channelId = video.channel.channel_id;
|
||||
const channelName = video.channel.channel_name;
|
||||
@ -99,7 +105,7 @@ const EmbeddableVideoPlayer = ({ videoId }: EmbeddableVideoPlayerProps) => {
|
||||
<div ref={inlinePlayerRef} className="player-wrapper">
|
||||
<div className="video-player">
|
||||
<VideoPlayer
|
||||
video={videoResponse}
|
||||
video={videoResponseData}
|
||||
sponsorBlock={sponsorblock}
|
||||
embed={true}
|
||||
autoplay={true}
|
||||
|
@ -22,7 +22,11 @@ const Filterbar = ({ hideToggleText, viewStyleName, showSort = true }: Filterbar
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
setUserConfig(updatedUserConfig);
|
||||
const { data: updatedUserConfigData } = updatedUserConfig;
|
||||
|
||||
if (updatedUserConfigData) {
|
||||
setUserConfig(updatedUserConfigData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,11 @@
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import loadNotifications, { NotificationPages } from '../api/loader/loadNotifications';
|
||||
import loadNotifications, {
|
||||
NotificationPages,
|
||||
NotificationResponseType,
|
||||
} from '../api/loader/loadNotifications';
|
||||
import iconStop from '/img/icon-stop.svg';
|
||||
import stopTaskByName from '../api/actions/stopTaskByName';
|
||||
|
||||
type NotificationType = {
|
||||
title: string;
|
||||
group: string;
|
||||
api_stop: boolean;
|
||||
level: string;
|
||||
id: string;
|
||||
command: boolean | string;
|
||||
messages: string[];
|
||||
progress: number;
|
||||
};
|
||||
|
||||
type NotificationResponseType = NotificationType[];
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
type NotificationsProps = {
|
||||
pageName: NotificationPages;
|
||||
@ -29,13 +20,17 @@ const Notifications = ({
|
||||
update,
|
||||
setShouldRefresh,
|
||||
}: NotificationsProps) => {
|
||||
const [notificationResponse, setNotificationResponse] = useState<NotificationResponseType>([]);
|
||||
const [notificationResponse, setNotificationResponse] =
|
||||
useState<ApiResponseType<NotificationResponseType>>();
|
||||
|
||||
const { data: notificationResponseData } = notificationResponse ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(async () => {
|
||||
const notifications = await loadNotifications(pageName, includeReindex);
|
||||
const { data: notificationsData } = notifications ?? {};
|
||||
|
||||
if (notifications.length === 0) {
|
||||
if (notificationsData?.length === 0) {
|
||||
setNotificationResponse(notifications);
|
||||
clearInterval(intervalId);
|
||||
setShouldRefresh?.(true);
|
||||
@ -52,13 +47,13 @@ const Notifications = ({
|
||||
};
|
||||
}, [pageName, update, setShouldRefresh, includeReindex]);
|
||||
|
||||
if (notificationResponse.length === 0) {
|
||||
if (notificationResponseData?.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{notificationResponse.map(notification => (
|
||||
{notificationResponseData?.map(notification => (
|
||||
<div
|
||||
id={notification.id}
|
||||
className={`notification ${notification.level}`}
|
||||
|
@ -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 OverviewStatsProps = {
|
||||
videoStats?: VideoStatsType;
|
||||
useSI: boolean;
|
||||
useSIUnits: boolean;
|
||||
};
|
||||
|
||||
const OverviewStats = ({ videoStats, useSI }: OverviewStatsProps) => {
|
||||
const OverviewStats = ({ videoStats, useSIUnits }: OverviewStatsProps) => {
|
||||
if (!videoStats) {
|
||||
return <p id="loading">Loading...</p>;
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 <p id="loading">Loading...</p>;
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
|
@ -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';
|
||||
|
@ -15,10 +15,20 @@ export interface ApiError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const APIClient = async (
|
||||
export type ResponseErrorType = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ApiResponseType<T> = {
|
||||
data?: T;
|
||||
error?: ResponseErrorType;
|
||||
status: number;
|
||||
};
|
||||
|
||||
const APIClient = async <T>(
|
||||
endpoint: string,
|
||||
{ method = 'GET', body, headers = {}, ...options }: ApiClientOptions = {},
|
||||
) => {
|
||||
): Promise<ApiResponseType<T>> => {
|
||||
const apiUrl = getApiUrl();
|
||||
const csrfToken = getCookie('csrftoken');
|
||||
|
||||
@ -55,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;
|
||||
|
@ -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: <Login />,
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFound />,
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
],
|
||||
{ basename: import.meta.env.BASE_URL },
|
||||
);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
|
||||
import ChannelOverview from '../components/ChannelOverview';
|
||||
import { useEffect, useState } from 'react';
|
||||
import loadChannelById from '../api/loader/loadChannelById';
|
||||
import { ChannelResponseType } from './ChannelBase';
|
||||
import loadChannelById, { ChannelResponseType } from '../api/loader/loadChannelById';
|
||||
import Linkify from '../components/Linkify';
|
||||
import deleteChannel from '../api/actions/deleteChannel';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
@ -16,6 +15,7 @@ import useIsAdmin from '../functions/useIsAdmin';
|
||||
import InputConfig from '../components/InputConfig';
|
||||
import ToggleConfig from '../components/ToggleConfig';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
export type ChannelBaseOutletContextType = {
|
||||
currentPage: number;
|
||||
@ -45,7 +45,7 @@ const ChannelAbout = () => {
|
||||
const [reindex, setReindex] = useState(false);
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
|
||||
|
||||
const [downloadFormat, setDownloadFormat] = useState<string | null>(null);
|
||||
const [autoDeleteAfter, setAutoDeleteAfter] = useState<number | null>(null);
|
||||
@ -55,24 +55,31 @@ const ChannelAbout = () => {
|
||||
const [pageSizeShorts, setPageSizeShorts] = useState<number | null>(null);
|
||||
const [pageSizeStreams, setPageSizeStreams] = useState<number | null>(null);
|
||||
|
||||
const channel = channelResponse;
|
||||
const { data: channelResponseData } = channelResponse ?? {};
|
||||
|
||||
const channel = channelResponseData;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (refresh) {
|
||||
const channelResponse = await loadChannelById(channelId);
|
||||
const { data: channelResponseData } = channelResponse;
|
||||
|
||||
setChannelResponse(channelResponse);
|
||||
setDownloadFormat(channelResponse?.channel_overwrites?.download_format ?? null);
|
||||
setAutoDeleteAfter(channelResponse?.channel_overwrites?.autodelete_days ?? null);
|
||||
setIndexPlaylists(channelResponse?.channel_overwrites?.index_playlists ?? false);
|
||||
setEnableSponsorblock(channelResponse?.channel_overwrites?.integrate_sponsorblock ?? null);
|
||||
setPageSizeVideo(channelResponse?.channel_overwrites?.subscriptions_channel_size ?? null);
|
||||
setDownloadFormat(channelResponseData?.channel_overwrites?.download_format ?? null);
|
||||
setAutoDeleteAfter(channelResponseData?.channel_overwrites?.autodelete_days ?? null);
|
||||
setIndexPlaylists(channelResponseData?.channel_overwrites?.index_playlists ?? false);
|
||||
setEnableSponsorblock(
|
||||
channelResponseData?.channel_overwrites?.integrate_sponsorblock ?? null,
|
||||
);
|
||||
setPageSizeVideo(
|
||||
channelResponseData?.channel_overwrites?.subscriptions_channel_size ?? null,
|
||||
);
|
||||
setPageSizeShorts(
|
||||
channelResponse?.channel_overwrites?.subscriptions_shorts_channel_size ?? null,
|
||||
channelResponseData?.channel_overwrites?.subscriptions_shorts_channel_size ?? null,
|
||||
);
|
||||
setPageSizeStreams(
|
||||
channelResponse?.channel_overwrites?.subscriptions_live_channel_size ?? null,
|
||||
channelResponseData?.channel_overwrites?.subscriptions_live_channel_size ?? null,
|
||||
);
|
||||
|
||||
setRefresh(false);
|
||||
|
@ -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<ChannelResponseType>();
|
||||
const [channelNav, setChannelNav] = useState<ChannelNavResponseType>();
|
||||
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
|
||||
const [channelNav, setChannelNav] = useState<ApiResponseType<ChannelNavResponseType>>();
|
||||
const [startNotification, setStartNotification] = useState(false);
|
||||
|
||||
const channel = channelResponse;
|
||||
const { has_streams, has_shorts, has_playlists, has_pending } = channelNav || {};
|
||||
const { data: channelResponseData, error: channelResponseError } = channelResponse ?? {};
|
||||
const { data: channelNavData } = channelNav ?? {};
|
||||
|
||||
const channel = channelResponseData;
|
||||
const { has_streams, has_shorts, has_playlists, has_pending } = channelNavData || {};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
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 <NotFound failType="channel" />;
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
return [];
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import { useEffect, useState } from 'react';
|
||||
import { OutletContextType } from './Base';
|
||||
import Pagination from '../components/Pagination';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
||||
import { PlaylistsResponseType } from './Playlists';
|
||||
import loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList';
|
||||
import iconGridView from '/img/icon-gridview.svg';
|
||||
import iconListView from '/img/icon-listview.svg';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
const ChannelPlaylist = () => {
|
||||
const { channelId } = useParams();
|
||||
@ -19,17 +19,24 @@ const ChannelPlaylist = () => {
|
||||
|
||||
const [refreshPlaylists, setRefreshPlaylists] = useState(false);
|
||||
|
||||
const [playlistsResponse, setPlaylistsResponse] = useState<PlaylistsResponseType>();
|
||||
const [playlistsResponse, setPlaylistsResponse] =
|
||||
useState<ApiResponseType<PlaylistsResponseType>>();
|
||||
|
||||
const playlistList = playlistsResponse?.data;
|
||||
const pagination = playlistsResponse?.paginate;
|
||||
const { data: playlistsResponseData } = playlistsResponse ?? {};
|
||||
|
||||
const playlistList = playlistsResponseData?.data;
|
||||
const pagination = playlistsResponseData?.paginate;
|
||||
|
||||
const view = userConfig.view_style_playlist;
|
||||
const showSubedOnly = userConfig.show_subed_only;
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
setUserConfig(updatedUserConfig);
|
||||
const { data: updatedUserConfigData } = updatedUserConfig;
|
||||
|
||||
if (updatedUserConfigData) {
|
||||
setUserConfig(updatedUserConfigData);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -37,6 +44,7 @@ const ChannelPlaylist = () => {
|
||||
const playlists = await loadPlaylistList({
|
||||
channel: channelId,
|
||||
subscribed: showSubedOnly,
|
||||
page: currentPage,
|
||||
});
|
||||
|
||||
setPlaylistsResponse(playlists);
|
||||
|
@ -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<ChannelResponseType>();
|
||||
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
||||
const [videoAggsResponse, setVideoAggsResponse] = useState<ChannelAggsType>();
|
||||
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
|
||||
const [videoResponse, setVideoReponse] =
|
||||
useState<ApiResponseType<VideoListByFilterResponseType>>();
|
||||
const [videoAggsResponse, setVideoAggsResponse] = useState<ApiResponseType<ChannelAggsType>>();
|
||||
|
||||
const channel = channelResponse;
|
||||
const videoList = videoResponse?.data;
|
||||
const pagination = videoResponse?.paginate;
|
||||
const { data: channelResponseData } = channelResponse ?? {};
|
||||
const { data: videoResponseData } = videoResponse ?? {};
|
||||
const { data: videoAggsResponseData } = videoAggsResponse ?? {};
|
||||
|
||||
const hasVideos = videoResponse?.data?.length !== 0;
|
||||
const channel = channelResponseData;
|
||||
const videoList = videoResponseData?.data;
|
||||
const pagination = videoResponseData?.paginate;
|
||||
const videoAggs = videoAggsResponseData;
|
||||
|
||||
const hasVideos = videoResponseData?.data?.length !== 0;
|
||||
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
||||
|
||||
const view = userConfig.view_style_home;
|
||||
const isGridView = view === ViewStyles.grid;
|
||||
@ -107,14 +115,13 @@ const ChannelVideo = ({ videoType }: ChannelVideoProps) => {
|
||||
setRefresh={setRefresh}
|
||||
/>
|
||||
<div className="info-box-item">
|
||||
{videoAggsResponse && (
|
||||
{videoAggs && (
|
||||
<>
|
||||
<p>
|
||||
{videoAggsResponse.total_items.value} videos{' '}
|
||||
<span className="space-carrot">|</span>{' '}
|
||||
{videoAggsResponse.total_duration.value_str} playback{' '}
|
||||
{videoAggs.total_items.value} videos <span className="space-carrot">|</span>{' '}
|
||||
{videoAggs.total_duration.value_str} playback{' '}
|
||||
<span className="space-carrot">|</span> Total size{' '}
|
||||
{humanFileSize(videoAggsResponse.total_size.value, true)}
|
||||
{humanFileSize(videoAggs.total_size.value, useSiUnits)}
|
||||
</p>
|
||||
<div className="button-box">
|
||||
<Button
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import loadChannelList from '../api/loader/loadChannelList';
|
||||
import loadChannelList, { ChannelsListResponse } from '../api/loader/loadChannelList';
|
||||
import iconGridView from '/img/icon-gridview.svg';
|
||||
import iconListView from '/img/icon-listview.svg';
|
||||
import iconAdd from '/img/icon-add.svg';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Pagination, { PaginationType } from '../components/Pagination';
|
||||
import { ConfigType } from './Home';
|
||||
import Pagination from '../components/Pagination';
|
||||
import { OutletContextType } from './Base';
|
||||
import ChannelList from '../components/ChannelList';
|
||||
import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
@ -15,6 +14,7 @@ import updateBulkChannelSubscriptions from '../api/actions/updateBulkChannelSubs
|
||||
import useIsAdmin from '../functions/useIsAdmin';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
type ChannelOverwritesType = {
|
||||
download_format: string | null;
|
||||
@ -42,31 +42,31 @@ export type ChannelType = {
|
||||
channel_views: number;
|
||||
};
|
||||
|
||||
type ChannelsListResponse = {
|
||||
data: ChannelType[];
|
||||
paginate: PaginationType;
|
||||
config?: ConfigType;
|
||||
};
|
||||
|
||||
const Channels = () => {
|
||||
const { userConfig, setUserConfig } = useUserConfigStore();
|
||||
const { currentPage, setCurrentPage } = useOutletContext() as OutletContextType;
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const [channelListResponse, setChannelListResponse] = useState<ChannelsListResponse>();
|
||||
const [channelListResponse, setChannelListResponse] =
|
||||
useState<ApiResponseType<ChannelsListResponse>>();
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
const [showNotification, setShowNotification] = useState(false);
|
||||
const [channelsToSubscribeTo, setChannelsToSubscribeTo] = useState('');
|
||||
|
||||
const channels = channelListResponse?.data;
|
||||
const pagination = channelListResponse?.paginate;
|
||||
// const channelCount = pagination?.total_hits;
|
||||
const { data: channelListResponseData } = channelListResponse ?? {};
|
||||
|
||||
const channels = channelListResponseData?.data;
|
||||
const pagination = channelListResponseData?.paginate;
|
||||
const hasChannels = channels?.length !== 0;
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
setUserConfig(updatedUserConfig);
|
||||
const { data: updatedUserConfigData } = updatedUserConfig;
|
||||
|
||||
if (updatedUserConfigData) {
|
||||
setUserConfig(updatedUserConfigData);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -187,7 +187,6 @@ const Channels = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* {hasChannels && <h2>Total channels: {channelCount}</h2>} */}
|
||||
|
||||
<div className={`channel-list ${userConfig.view_style_channel}`}>
|
||||
{!hasChannels && <h2>No channels found...</h2>}
|
||||
|
@ -20,6 +20,7 @@ import DownloadListItem from '../components/DownloadListItem';
|
||||
import loadDownloadAggs, { DownloadAggsType } from '../api/loader/loadDownloadAggs';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
type Download = {
|
||||
auto_start: boolean;
|
||||
@ -61,18 +62,22 @@ const Download = () => {
|
||||
|
||||
const [downloadQueueText, setDownloadQueueText] = useState('');
|
||||
|
||||
const [downloadResponse, setDownloadResponse] = useState<DownloadResponseType>();
|
||||
const [downloadAggsResponse, setDownloadAggsResponse] = useState<DownloadAggsType>();
|
||||
const [downloadResponse, setDownloadResponse] = useState<ApiResponseType<DownloadResponseType>>();
|
||||
const [downloadAggsResponse, setDownloadAggsResponse] =
|
||||
useState<ApiResponseType<DownloadAggsType>>();
|
||||
|
||||
const downloadList = downloadResponse?.data;
|
||||
const pagination = downloadResponse?.paginate;
|
||||
const channelAggsList = downloadAggsResponse?.buckets;
|
||||
const { data: downloadResponseData } = downloadResponse ?? {};
|
||||
const { data: downloadAggsResponseData } = downloadAggsResponse ?? {};
|
||||
|
||||
const downloadList = downloadResponseData?.data;
|
||||
const pagination = downloadResponseData?.paginate;
|
||||
const channelAggsList = downloadAggsResponseData?.buckets;
|
||||
|
||||
const downloadCount = pagination?.total_hits;
|
||||
|
||||
const channel_filter_name =
|
||||
downloadResponse?.data?.length && downloadResponse?.data?.length > 0
|
||||
? downloadResponse?.data[0].channel_name
|
||||
downloadResponseData?.data?.length && downloadResponseData?.data?.length > 0
|
||||
? downloadResponseData?.data[0].channel_name
|
||||
: '';
|
||||
|
||||
const view = userConfig.view_style_downloads;
|
||||
@ -84,19 +89,28 @@ const Download = () => {
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
setUserConfig(updatedUserConfig);
|
||||
const { data: updatedUserConfigData } = updatedUserConfig;
|
||||
|
||||
if (updatedUserConfigData) {
|
||||
setUserConfig(updatedUserConfigData);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const videos = await loadDownloadQueue(currentPage, channelFilterFromUrl, showIgnored);
|
||||
const videoCount = videos?.paginate?.total_hits;
|
||||
const videosResponse = await loadDownloadQueue(
|
||||
currentPage,
|
||||
channelFilterFromUrl,
|
||||
showIgnored,
|
||||
);
|
||||
const { data: channelResponseData } = videosResponse ?? {};
|
||||
const videoCount = channelResponseData?.paginate?.total_hits;
|
||||
|
||||
if (videoCount && lastVideoCount !== videoCount) {
|
||||
setLastVideoCount(videoCount);
|
||||
}
|
||||
|
||||
setDownloadResponse(videos);
|
||||
setDownloadResponse(videosResponse);
|
||||
setRefresh(false);
|
||||
})();
|
||||
|
||||
|
@ -14,6 +14,7 @@ import ScrollToTopOnNavigate from '../components/ScrollToTop';
|
||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||
import { SponsorBlockType } from './Video';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
export type PlayerType = {
|
||||
watched: boolean;
|
||||
@ -110,15 +111,19 @@ const Home = () => {
|
||||
|
||||
const [refreshVideoList, setRefreshVideoList] = useState(false);
|
||||
|
||||
const [videoResponse, setVideoReponse] = useState<VideoListByFilterResponseType>();
|
||||
const [videoResponse, setVideoReponse] =
|
||||
useState<ApiResponseType<VideoListByFilterResponseType>>();
|
||||
const [continueVideoResponse, setContinueVideoResponse] =
|
||||
useState<VideoListByFilterResponseType>();
|
||||
useState<ApiResponseType<VideoListByFilterResponseType>>();
|
||||
|
||||
const videoList = videoResponse?.data;
|
||||
const pagination = videoResponse?.paginate;
|
||||
const continueVideos = continueVideoResponse?.data;
|
||||
const { data: videoResponseData } = videoResponse ?? {};
|
||||
const { data: continueVideoResponseData } = continueVideoResponse ?? {};
|
||||
|
||||
const hasVideos = videoResponse?.data?.length !== 0;
|
||||
const videoList = videoResponseData?.data;
|
||||
const pagination = videoResponseData?.paginate;
|
||||
const continueVideos = continueVideoResponseData?.data;
|
||||
|
||||
const hasVideos = videoResponseData?.data?.length !== 0;
|
||||
|
||||
const isGridView = userConfig.view_style_home === ViewStyles.grid;
|
||||
const gridView = isGridView ? `boxed-${userConfig.grid_items}` : '';
|
||||
|
@ -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<string | null>(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 && <Button label="Login" type="submit" />}
|
||||
</form>
|
||||
|
||||
{waitedCount > 10 && (
|
||||
<div className="info-box">
|
||||
<div className="info-box-item">
|
||||
<h2>Having issues?</h2>
|
||||
|
||||
<div className="help-text left-align">
|
||||
<p>Please verify that you setup your environment correctly:</p>
|
||||
<ul>
|
||||
<li
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`TA_HOST=${window.location.origin}`);
|
||||
}}
|
||||
>
|
||||
TA_HOST={window.location.origin}
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText('REDIS_CON=redis://archivist-redis:6379');
|
||||
}}
|
||||
>
|
||||
REDIS_CON=redis://archivist-redis:6379
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="login-links">
|
||||
<span>
|
||||
<a href="https://github.com/tubearchivist/tubearchivist" target="_blank">
|
||||
|
23
frontend/src/pages/NotFound.tsx
Normal file
23
frontend/src/pages/NotFound.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import useColours from '../configuration/colours/useColours';
|
||||
import Routes from '../configuration/routes/RouteList';
|
||||
|
||||
const NotFound = ({ failType = 'page' }) => {
|
||||
useColours();
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>404 | Not found</title>
|
||||
<div id="error-page" style={{ margin: '10%' }}>
|
||||
<h1>Oops!</h1>
|
||||
<p>
|
||||
<i>404</i>
|
||||
<span>: That {failType} does not exist.</span>
|
||||
</p>
|
||||
<Link to={Routes.Home}>Go Home</Link>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
@ -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,6 +22,8 @@ import Button from '../components/Button';
|
||||
import loadVideoListByFilter from '../api/loader/loadVideoListByPage';
|
||||
import useIsAdmin from '../functions/useIsAdmin';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
import NotFound from './NotFound';
|
||||
|
||||
export type VideoResponseType = {
|
||||
data?: VideoType[];
|
||||
@ -44,16 +45,20 @@ const Playlist = () => {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [reindex, setReindex] = useState(false);
|
||||
|
||||
const [playlistResponse, setPlaylistResponse] = useState<PlaylistResponseType>();
|
||||
const [channelResponse, setChannelResponse] = useState<ChannelResponseType>();
|
||||
const [videoResponse, setVideoResponse] = useState<VideoResponseType>();
|
||||
const [playlistResponse, setPlaylistResponse] = useState<ApiResponseType<PlaylistResponseType>>();
|
||||
const [channelResponse, setChannelResponse] = useState<ApiResponseType<ChannelResponseType>>();
|
||||
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
|
||||
|
||||
const playlist = playlistResponse;
|
||||
const channel = channelResponse;
|
||||
const videos = videoResponse?.data;
|
||||
const pagination = videoResponse?.paginate;
|
||||
const { data: playlistResponseData, error: playlistResponseError } = playlistResponse ?? {};
|
||||
const { data: channelResponseData } = channelResponse ?? {};
|
||||
const { data: videoResponseData } = videoResponse ?? {};
|
||||
|
||||
const palylistEntries = playlistResponse?.playlist_entries;
|
||||
const playlist = playlistResponseData;
|
||||
const channel = channelResponseData;
|
||||
const videos = videoResponseData?.data;
|
||||
const pagination = videoResponseData?.paginate;
|
||||
|
||||
const palylistEntries = playlistResponseData?.playlist_entries;
|
||||
const videoArchivedCount = Number(palylistEntries?.filter(video => video.downloaded).length);
|
||||
const videoInPlaylistCount = pagination?.total_hits;
|
||||
|
||||
@ -71,18 +76,18 @@ const Playlist = () => {
|
||||
playlist: playlistId,
|
||||
page: currentPage,
|
||||
watch: hideWatched ? 'unwatched' : undefined,
|
||||
sort: 'downloaded', // downloaded or published? or playlist sort order?
|
||||
});
|
||||
|
||||
const isCustomPlaylist = playlist?.playlist_type === 'custom';
|
||||
if (!isCustomPlaylist) {
|
||||
const channel = await loadChannelById(playlist.playlist_channel_id);
|
||||
|
||||
setChannelResponse(channel);
|
||||
}
|
||||
|
||||
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);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -95,10 +100,14 @@ const Playlist = () => {
|
||||
videoId,
|
||||
]);
|
||||
|
||||
if (!playlistId || !playlist) {
|
||||
return `Playlist ${playlistId} not found!`;
|
||||
const errorMessage = playlistResponseError?.error;
|
||||
|
||||
if (errorMessage) {
|
||||
return <NotFound failType="playlist" />;
|
||||
}
|
||||
|
||||
if (!playlist || !playlistId) return [];
|
||||
|
||||
const isCustomPlaylist = playlist.playlist_type === 'custom';
|
||||
|
||||
return (
|
||||
|
@ -6,8 +6,8 @@ import iconGridView from '/img/icon-gridview.svg';
|
||||
import iconListView from '/img/icon-listview.svg';
|
||||
|
||||
import { OutletContextType } from './Base';
|
||||
import loadPlaylistList from '../api/loader/loadPlaylistList';
|
||||
import Pagination, { PaginationType } from '../components/Pagination';
|
||||
import loadPlaylistList, { PlaylistsResponseType } from '../api/loader/loadPlaylistList';
|
||||
import Pagination from '../components/Pagination';
|
||||
import PlaylistList from '../components/PlaylistList';
|
||||
import updateBulkPlaylistSubscriptions from '../api/actions/updateBulkPlaylistSubscriptions';
|
||||
import createCustomPlaylist from '../api/actions/createCustomPlaylist';
|
||||
@ -17,12 +17,7 @@ import useIsAdmin from '../functions/useIsAdmin';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import Notifications from '../components/Notifications';
|
||||
import updateUserConfig, { UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import { PlaylistType } from '../api/loader/loadPlaylistById';
|
||||
|
||||
export type PlaylistsResponseType = {
|
||||
data?: PlaylistType[];
|
||||
paginate?: PaginationType;
|
||||
};
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
const Playlists = () => {
|
||||
const { userConfig, setUserConfig } = useUserConfigStore();
|
||||
@ -35,12 +30,14 @@ const Playlists = () => {
|
||||
const [playlistsToAddText, setPlaylistsToAddText] = useState('');
|
||||
const [customPlaylistsToAddText, setCustomPlaylistsToAddText] = useState('');
|
||||
|
||||
const [playlistResponse, setPlaylistReponse] = useState<PlaylistsResponseType>();
|
||||
const [playlistResponse, setPlaylistReponse] = useState<ApiResponseType<PlaylistsResponseType>>();
|
||||
|
||||
const playlistList = playlistResponse?.data;
|
||||
const pagination = playlistResponse?.paginate;
|
||||
const { data: playlistResponseData } = playlistResponse ?? {};
|
||||
|
||||
const hasPlaylists = playlistResponse?.data?.length !== 0;
|
||||
const playlistList = playlistResponseData?.data;
|
||||
const pagination = playlistResponseData?.paginate;
|
||||
|
||||
const hasPlaylists = playlistResponseData?.data?.length !== 0;
|
||||
|
||||
const view = userConfig.view_style_playlist;
|
||||
const showSubedOnly = userConfig.show_subed_only;
|
||||
@ -61,7 +58,11 @@ const Playlists = () => {
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
setUserConfig(updatedUserConfig);
|
||||
const { data: updatedUserConfigData } = updatedUserConfig;
|
||||
|
||||
if (updatedUserConfigData) {
|
||||
setUserConfig(updatedUserConfigData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { VideoType } from './Home';
|
||||
import loadSearch from '../api/loader/loadSearch';
|
||||
import { ChannelType } from './Channels';
|
||||
import loadSearch, { SearchResultsType } from '../api/loader/loadSearch';
|
||||
import VideoList from '../components/VideoList';
|
||||
import ChannelList from '../components/ChannelList';
|
||||
import PlaylistList from '../components/PlaylistList';
|
||||
@ -11,28 +9,20 @@ import { ViewStyles } from '../configuration/constants/ViewStyle';
|
||||
import EmbeddableVideoPlayer from '../components/EmbeddableVideoPlayer';
|
||||
import SearchExampleQueries from '../components/SearchExampleQueries';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { PlaylistType } from '../api/loader/loadPlaylistById';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
const EmptySearchResponse: SearchResultsType = {
|
||||
results: {
|
||||
video_results: [],
|
||||
channel_results: [],
|
||||
playlist_results: [],
|
||||
fulltext_results: [],
|
||||
const EmptySearchResponse: ApiResponseType<SearchResultsType> = {
|
||||
data: {
|
||||
results: {
|
||||
video_results: [],
|
||||
channel_results: [],
|
||||
playlist_results: [],
|
||||
fulltext_results: [],
|
||||
},
|
||||
queryType: 'simple',
|
||||
},
|
||||
queryType: 'simple',
|
||||
};
|
||||
|
||||
type SearchResultType = {
|
||||
video_results: VideoType[];
|
||||
channel_results: ChannelType[];
|
||||
playlist_results: PlaylistType[];
|
||||
fulltext_results: [];
|
||||
};
|
||||
|
||||
type SearchResultsType = {
|
||||
results: SearchResultType;
|
||||
queryType: string;
|
||||
error: undefined,
|
||||
status: 200,
|
||||
};
|
||||
|
||||
const Search = () => {
|
||||
@ -47,15 +37,17 @@ const Search = () => {
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState<string>('');
|
||||
const [searchResults, setSearchResults] = useState<SearchResultsType>();
|
||||
const [searchResults, setSearchResults] = useState<ApiResponseType<SearchResultsType>>();
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const videoList = searchResults?.results.video_results;
|
||||
const channelList = searchResults?.results.channel_results;
|
||||
const playlistList = searchResults?.results.playlist_results;
|
||||
const fulltextList = searchResults?.results.fulltext_results;
|
||||
const queryType = searchResults?.queryType;
|
||||
const { data: searchResultsData } = searchResults ?? {};
|
||||
|
||||
const videoList = searchResultsData?.results.video_results;
|
||||
const channelList = searchResultsData?.results.channel_results;
|
||||
const playlistList = searchResultsData?.results.playlist_results;
|
||||
const fulltextList = searchResultsData?.results.fulltext_results;
|
||||
const queryType = searchResultsData?.queryType;
|
||||
|
||||
const hasSearchQuery = searchTerm.length > 0;
|
||||
const hasVideos = Number(videoList?.length) > 0;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import loadBackupList from '../api/loader/loadBackupList';
|
||||
import loadBackupList, { BackupListType } from '../api/loader/loadBackupList';
|
||||
import SettingsNavigation from '../components/SettingsNavigation';
|
||||
import deleteDownloadQueueByFilter from '../api/actions/deleteDownloadQueueByFilter';
|
||||
import updateTaskByName from '../api/actions/updateTaskByName';
|
||||
@ -7,16 +7,7 @@ import queueBackup from '../api/actions/queueBackup';
|
||||
import restoreBackup from '../api/actions/restoreBackup';
|
||||
import Notifications from '../components/Notifications';
|
||||
import Button from '../components/Button';
|
||||
|
||||
type Backup = {
|
||||
filename: string;
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
timestamp: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
type BackupListType = Backup[];
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
const SettingsActions = () => {
|
||||
const [deleteIgnored, setDeleteIgnored] = useState(false);
|
||||
@ -27,9 +18,11 @@ const SettingsActions = () => {
|
||||
const [isRestoringBackup, setIsRestoringBackup] = useState(false);
|
||||
const [reScanningFileSystem, setReScanningFileSystem] = useState(false);
|
||||
|
||||
const [backupListResponse, setBackupListResponse] = useState<BackupListType>();
|
||||
const [backupListResponse, setBackupListResponse] = useState<ApiResponseType<BackupListType>>();
|
||||
|
||||
const backups = backupListResponse;
|
||||
const { data: backupListResponseData } = backupListResponse ?? {};
|
||||
|
||||
const backups = backupListResponseData;
|
||||
const hasBackups = !!backups && backups?.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
@ -15,131 +21,38 @@ import DownloadHistoryStats from '../components/DownloadHistoryStats';
|
||||
import BiggestChannelsStats from '../components/BiggestChannelsStats';
|
||||
import Notifications from '../components/Notifications';
|
||||
import PaginationDummy from '../components/PaginationDummy';
|
||||
|
||||
export type VideoStatsType = {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
type_videos: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
type_shorts: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
active_true: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
active_false: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
type_streams: {
|
||||
doc_count: number;
|
||||
media_size: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ChannelStatsType = {
|
||||
doc_count: number;
|
||||
active_true: number;
|
||||
subscribed_true: number;
|
||||
};
|
||||
|
||||
export type PlaylistStatsType = {
|
||||
doc_count: number;
|
||||
active_false: number;
|
||||
active_true: number;
|
||||
subscribed_true: number;
|
||||
};
|
||||
|
||||
export type DownloadStatsType = {
|
||||
pending: number;
|
||||
pending_videos: number;
|
||||
pending_shorts: number;
|
||||
pending_streams: number;
|
||||
};
|
||||
|
||||
export type WatchProgressStatsType = {
|
||||
total: {
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
items: number;
|
||||
};
|
||||
unwatched: {
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
progress: number;
|
||||
items: number;
|
||||
};
|
||||
watched: {
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
progress: number;
|
||||
items: number;
|
||||
};
|
||||
};
|
||||
|
||||
type DownloadHistoryType = {
|
||||
date: string;
|
||||
count: number;
|
||||
media_size: number;
|
||||
};
|
||||
|
||||
export type DownloadHistoryStatsType = DownloadHistoryType[];
|
||||
|
||||
type BiggestChannelsType = {
|
||||
id: string;
|
||||
name: string;
|
||||
doc_count: number;
|
||||
duration: number;
|
||||
duration_str: string;
|
||||
media_size: number;
|
||||
};
|
||||
|
||||
export type BiggestChannelsStatsType = BiggestChannelsType[];
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { FileSizeUnits } from '../api/actions/updateUserConfig';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
type DashboardStatsReponses = {
|
||||
videoStats?: VideoStatsType;
|
||||
channelStats?: ChannelStatsType;
|
||||
playlistStats?: PlaylistStatsType;
|
||||
downloadStats?: DownloadStatsType;
|
||||
watchProgressStats?: WatchProgressStatsType;
|
||||
downloadHistoryStats?: DownloadHistoryStatsType;
|
||||
biggestChannelsStatsByCount?: BiggestChannelsStatsType;
|
||||
biggestChannelsStatsByDuration?: BiggestChannelsStatsType;
|
||||
biggestChannelsStatsByMediaSize?: BiggestChannelsStatsType;
|
||||
videoStats?: ApiResponseType<VideoStatsType>;
|
||||
channelStats?: ApiResponseType<ChannelStatsType>;
|
||||
playlistStats?: ApiResponseType<PlaylistStatsType>;
|
||||
downloadStats?: ApiResponseType<DownloadStatsType>;
|
||||
watchProgressStats?: ApiResponseType<WatchProgressStatsType>;
|
||||
downloadHistoryStats?: ApiResponseType<DownloadHistoryStatsType>;
|
||||
biggestChannelsStatsByCount?: ApiResponseType<BiggestChannelsStatsType>;
|
||||
biggestChannelsStatsByDuration?: ApiResponseType<BiggestChannelsStatsType>;
|
||||
biggestChannelsStatsByMediaSize?: ApiResponseType<BiggestChannelsStatsType>;
|
||||
};
|
||||
|
||||
const SettingsDashboard = () => {
|
||||
const [useSi, setUseSi] = useState(false);
|
||||
const { userConfig } = useUserConfigStore();
|
||||
|
||||
const [response, setResponse] = useState<DashboardStatsReponses>({
|
||||
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 () => {
|
||||
@ -181,6 +94,8 @@ const SettingsDashboard = () => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>TA | Settings Dashboard</title>
|
||||
@ -190,31 +105,17 @@ const SettingsDashboard = () => {
|
||||
<div className="title-bar">
|
||||
<h1>Your Archive</h1>
|
||||
</div>
|
||||
<p>
|
||||
File Sizes in:
|
||||
<select
|
||||
value={useSi ? 'true' : 'false'}
|
||||
onChange={event => {
|
||||
const value = event.target.value;
|
||||
console.log(value);
|
||||
setUseSi(value === 'true');
|
||||
}}
|
||||
>
|
||||
<option value="true">SI units</option>
|
||||
<option value="false">Binary units</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<div className="settings-item">
|
||||
<h2>Overview</h2>
|
||||
<div className="info-box info-box-3">
|
||||
<OverviewStats videoStats={videoStats} useSI={useSi} />
|
||||
<OverviewStats videoStats={videoStats} useSIUnits={useSiUnits} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
<h2>Video Type</h2>
|
||||
<div className="info-box info-box-3">
|
||||
<VideoTypeStats videoStats={videoStats} useSI={useSi} />
|
||||
<VideoTypeStats videoStats={videoStats} useSIUnits={useSiUnits} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
@ -236,7 +137,7 @@ const SettingsDashboard = () => {
|
||||
<div className="settings-item">
|
||||
<h2>Download History</h2>
|
||||
<div className="info-box info-box-4">
|
||||
<DownloadHistoryStats downloadHistoryStats={downloadHistoryStats} useSI={false} />
|
||||
<DownloadHistoryStats downloadHistoryStats={downloadHistoryStats} useSIUnits={false} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-item">
|
||||
@ -246,7 +147,7 @@ const SettingsDashboard = () => {
|
||||
biggestChannelsStatsByCount={biggestChannelsStatsByCount}
|
||||
biggestChannelsStatsByDuration={biggestChannelsStatsByDuration}
|
||||
biggestChannelsStatsByMediaSize={biggestChannelsStatsByMediaSize}
|
||||
useSI={useSi}
|
||||
useSIUnits={useSiUnits}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,13 +13,14 @@ import createAppriseNotificationUrl, {
|
||||
AppriseTaskNameType,
|
||||
} from '../api/actions/createAppriseNotificationUrl';
|
||||
import deleteAppriseNotificationUrl from '../api/actions/deleteAppriseNotificationUrl';
|
||||
import { ApiError } from '../functions/APIClient';
|
||||
import { ApiError, ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
const SettingsScheduling = () => {
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
|
||||
const [scheduleResponse, setScheduleResponse] = useState<ScheduleResponseType>([]);
|
||||
const [appriseNotification, setAppriseNotification] = useState<AppriseNotificationType>();
|
||||
const [scheduleResponse, setScheduleResponse] = useState<ApiResponseType<ScheduleResponseType>>();
|
||||
const [appriseNotification, setAppriseNotification] =
|
||||
useState<ApiResponseType<AppriseNotificationType>>();
|
||||
|
||||
const [updateSubscribed, setUpdateSubscribed] = useState<string | undefined>();
|
||||
const [downloadPending, setDownloadPending] = useState<string | undefined>();
|
||||
@ -36,6 +37,9 @@ const SettingsScheduling = () => {
|
||||
const [thumnailCheckError, setThumnailCheckError] = useState<string | null>(null);
|
||||
const [zipBackupError, setZipBackupError] = useState<string | null>(null);
|
||||
|
||||
const { data: scheduleResponseData } = scheduleResponse ?? {};
|
||||
const { data: appriseNotificationData } = appriseNotification ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (refresh) {
|
||||
@ -54,7 +58,7 @@ const SettingsScheduling = () => {
|
||||
setRefresh(true);
|
||||
}, []);
|
||||
|
||||
const groupedSchedules = Object.groupBy(scheduleResponse, ({ name }) => name);
|
||||
const groupedSchedules = Object.groupBy(scheduleResponseData || [], ({ name }) => name);
|
||||
|
||||
console.log(groupedSchedules);
|
||||
|
||||
@ -460,11 +464,11 @@ const SettingsScheduling = () => {
|
||||
<div className="settings-group">
|
||||
<h2>Add Notification URL</h2>
|
||||
<div className="settings-item">
|
||||
{!appriseNotification && <p>No notifications stored</p>}
|
||||
{appriseNotification && (
|
||||
{!appriseNotificationData && <p>No notifications stored</p>}
|
||||
{appriseNotificationData && (
|
||||
<>
|
||||
<div className="description-text">
|
||||
{Object.entries(appriseNotification)?.map(([key, { urls, title }]) => {
|
||||
{Object.entries(appriseNotificationData)?.map(([key, { urls, title }]) => {
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<h3>{title}</h3>
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import updateUserConfig, { ColourVariants, UserConfigType } from '../api/actions/updateUserConfig';
|
||||
import updateUserConfig, {
|
||||
ColourVariants,
|
||||
FileSizeUnits,
|
||||
UserConfigType,
|
||||
} from '../api/actions/updateUserConfig';
|
||||
import { ColourConstant } from '../configuration/colours/useColours';
|
||||
import SettingsNavigation from '../components/SettingsNavigation';
|
||||
import Notifications from '../components/Notifications';
|
||||
@ -18,14 +22,21 @@ const SettingsUser = () => {
|
||||
const [styleSheetRefresh, setStyleSheetRefresh] = useState(false);
|
||||
const [pageSize, setPageSize] = useState<number>(userConfig.page_size);
|
||||
const [showHelpText, setShowHelpText] = useState(userConfig.show_help_text);
|
||||
const [selectedFileSizeUnit, setSelectedFileSizeUnit] = useState(FileSizeUnits.Binary);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setStyleSheet(userConfig.stylesheet);
|
||||
setPageSize(userConfig.page_size);
|
||||
setShowHelpText(userConfig.show_help_text);
|
||||
setSelectedFileSizeUnit(userConfig.file_size_unit);
|
||||
})();
|
||||
}, [userConfig.page_size, userConfig.stylesheet, userConfig.show_help_text]);
|
||||
}, [
|
||||
userConfig.page_size,
|
||||
userConfig.stylesheet,
|
||||
userConfig.show_help_text,
|
||||
userConfig.file_size_unit,
|
||||
]);
|
||||
|
||||
const handleStyleSheetChange = async (selectedStyleSheet: ColourVariants) => {
|
||||
handleUserConfigUpdate({ stylesheet: selectedStyleSheet });
|
||||
@ -41,9 +52,17 @@ const SettingsUser = () => {
|
||||
handleUserConfigUpdate({ [configKey]: configValue });
|
||||
};
|
||||
|
||||
const handleFileSizeUnitChange = async (configKey: string, configValue: string) => {
|
||||
handleUserConfigUpdate({ [configKey]: configValue });
|
||||
};
|
||||
|
||||
const handleUserConfigUpdate = async (config: Partial<UserConfigType>) => {
|
||||
const updatedUserConfig = await updateUserConfig(config);
|
||||
setUserConfig(updatedUserConfig);
|
||||
const { data: updatedUserConfigData } = updatedUserConfig;
|
||||
|
||||
if (updatedUserConfigData) {
|
||||
setUserConfig(updatedUserConfigData);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageRefresh = () => {
|
||||
@ -88,6 +107,7 @@ const SettingsUser = () => {
|
||||
{styleSheetRefresh && <button onClick={handlePageRefresh}>Refresh</button>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-box-wrapper">
|
||||
<div>
|
||||
<p>Archive view page size</p>
|
||||
@ -113,18 +133,40 @@ const SettingsUser = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-box-wrapper">
|
||||
<div>
|
||||
<p>Show help text</p>
|
||||
</div>
|
||||
|
||||
<ToggleConfig
|
||||
name="show_help_text"
|
||||
value={showHelpText}
|
||||
updateCallback={handleShowHelpTextChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="settings-box-wrapper"
|
||||
title="Metric (SI) units, aka powers of 1000. Binary (IEC), aka powers of 1024."
|
||||
>
|
||||
<div>
|
||||
<p>File size units:</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={selectedFileSizeUnit}
|
||||
onChange={event => {
|
||||
handleFileSizeUnitChange('file_size_unit', event.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
<option value={FileSizeUnits.Metric}>SI units</option>
|
||||
<option value={FileSizeUnits.Binary}>Binary units</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="settings-group">
|
||||
|
@ -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<VideoResponseType>();
|
||||
const [simmilarVideos, setSimmilarVideos] = useState<VideoResponseType[]>();
|
||||
const [videoPlaylistNav, setVideoPlaylistNav] = useState<VideoNavResponseType[]>();
|
||||
const [customPlaylistsResponse, setCustomPlaylistsResponse] = useState<PlaylistsResponseType>();
|
||||
const [commentsResponse, setCommentsResponse] = useState<CommentsResponseType>();
|
||||
const [videoResponse, setVideoResponse] = useState<ApiResponseType<VideoResponseType>>();
|
||||
const [similarVideos, setSimilarVideos] = useState<ApiResponseType<VideoResponseType[]>>();
|
||||
const [videoPlaylistNav, setVideoPlaylistNav] =
|
||||
useState<ApiResponseType<VideoNavResponseType[]>>();
|
||||
const [customPlaylistsResponse, setCustomPlaylistsResponse] =
|
||||
useState<ApiResponseType<PlaylistsResponseType>>();
|
||||
const [commentsResponse, setCommentsResponse] = useState<ApiResponseType<CommentsResponseType>>();
|
||||
|
||||
const { data: videoResponseData, error: videoResponseError } = videoResponse ?? {};
|
||||
const { data: similarVideosResponseData } = similarVideos ?? {};
|
||||
const { data: videoPlaylistNavResponseData } = videoPlaylistNav ?? {};
|
||||
const { data: customPlaylistsResponseData } = customPlaylistsResponse ?? {};
|
||||
const { data: commentsResponseData } = commentsResponse ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (refreshVideoList || videoId !== videoResponse?.youtube_id) {
|
||||
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 <NotFound failType="video" />;
|
||||
}
|
||||
|
||||
if (videoResponseData === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const video = videoResponse;
|
||||
const watched = videoResponse.player.watched;
|
||||
const playlistNav = videoPlaylistNav;
|
||||
const sponsorBlock = videoResponse.sponsorblock;
|
||||
const customPlaylists = customPlaylistsResponse?.data;
|
||||
const video = videoResponseData;
|
||||
const watched = video.player.watched;
|
||||
const playlistNav = videoPlaylistNavResponseData;
|
||||
const sponsorBlock = video.sponsorblock;
|
||||
const customPlaylists = customPlaylistsResponseData?.data;
|
||||
const starRating = convertStarRating(video?.stats?.average_rating);
|
||||
const comments = commentsResponse;
|
||||
const comments = commentsResponseData;
|
||||
const useSiUnits = userConfig.file_size_unit === FileSizeUnits.Metric;
|
||||
|
||||
console.log('playlistNav', playlistNav);
|
||||
|
||||
@ -209,7 +219,7 @@ const Video = () => {
|
||||
<ScrollToTopOnNavigate />
|
||||
|
||||
<VideoPlayer
|
||||
video={videoResponse}
|
||||
video={video}
|
||||
sponsorBlock={sponsorBlock}
|
||||
autoplay={playlistAutoplay}
|
||||
onWatchStateChanged={() => {
|
||||
@ -358,7 +368,15 @@ const Video = () => {
|
||||
navigate(Routes.Channel(video.channel.channel_id));
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
label="Delete and Ignore"
|
||||
className="danger-button"
|
||||
onClick={async () => {
|
||||
await deleteVideo(videoId);
|
||||
await updateDownloadQueueStatusById(videoId, 'ignore-force');
|
||||
navigate(Routes.Channel(video.channel.channel_id));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Cancel"
|
||||
onClick={() => setShowDeleteConfirm(!showDeleteConfirm)}
|
||||
@ -430,14 +448,14 @@ const Video = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-box-item">
|
||||
{video.media_size && <p>File size: {humanFileSize(video.media_size)}</p>}
|
||||
{video.media_size && <p>File size: {humanFileSize(video.media_size, useSiUnits)}</p>}
|
||||
|
||||
{video.streams &&
|
||||
video.streams.map(stream => {
|
||||
return (
|
||||
<p key={stream.index}>
|
||||
{capitalizeFirstLetter(stream.type)}: {stream.codec}{' '}
|
||||
{humanFileSize(stream.bitrate)}/s
|
||||
{humanFileSize(stream.bitrate, useSiUnits)}/s
|
||||
{stream.width && (
|
||||
<>
|
||||
<span className="space-carrot">|</span> {stream.width}x{stream.height}
|
||||
@ -565,7 +583,7 @@ const Video = () => {
|
||||
<h3>Similar Videos</h3>
|
||||
<div className="video-list grid grid-3" id="similar-videos">
|
||||
<VideoList
|
||||
videoList={simmilarVideos}
|
||||
videoList={similarVideosResponseData}
|
||||
viewLayout="grid"
|
||||
refreshVideoList={setRefreshVideoList}
|
||||
/>
|
||||
|
@ -18,6 +18,7 @@ export const useUserConfigStore = create<UserConfigState>(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,
|
||||
|
@ -179,6 +179,10 @@ button:hover {
|
||||
padding: 5px 0 5px 1rem;
|
||||
}
|
||||
|
||||
.left-align {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.help-text::before {
|
||||
content: '?';
|
||||
font-size: 1.5em;
|
||||
|
@ -15,4 +15,7 @@ export default defineConfig({
|
||||
usePolling: true,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user