From e0969a1d78507978292972ee186ca207514b3492 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Thu, 2 Jul 2026 19:46:19 +0100 Subject: [PATCH] refactor(frontend): split endpoint handling --- frontend/src/components/Topbar.tsx | 2 +- frontend/src/components/video/VideoCard.tsx | 2 +- frontend/src/layouts/MainLayout.tsx | 3 +- frontend/src/pages/ClipEdit.tsx | 3 +- frontend/src/pages/EditClip.tsx | 2 +- frontend/src/pages/EditVod.tsx | 2 +- frontend/src/pages/MyClips.tsx | 2 +- frontend/src/pages/MyVods.tsx | 2 +- frontend/src/pages/Profile.tsx | 2 +- frontend/src/pages/StreamControl.tsx | 17 +- frontend/src/pages/VideoPlayer.tsx | 3 +- frontend/src/pages/VodPlayer.tsx | 3 +- frontend/src/utils/api/client.ts | 12 + frontend/src/utils/api/clips.ts | 67 ++++ frontend/src/utils/api/jobs.ts | 15 + frontend/src/utils/api/markers.ts | 40 +++ frontend/src/utils/api/media.ts | 72 ++++ frontend/src/utils/api/stream.ts | 31 ++ frontend/src/utils/api/users.ts | 42 +++ frontend/src/utils/api/vods.ts | 67 ++++ frontend/src/utils/endpoints.ts | 346 -------------------- 21 files changed, 366 insertions(+), 369 deletions(-) create mode 100644 frontend/src/utils/api/client.ts create mode 100644 frontend/src/utils/api/clips.ts create mode 100644 frontend/src/utils/api/jobs.ts create mode 100644 frontend/src/utils/api/markers.ts create mode 100644 frontend/src/utils/api/media.ts create mode 100644 frontend/src/utils/api/stream.ts create mode 100644 frontend/src/utils/api/users.ts create mode 100644 frontend/src/utils/api/vods.ts delete mode 100644 frontend/src/utils/endpoints.ts diff --git a/frontend/src/components/Topbar.tsx b/frontend/src/components/Topbar.tsx index d3bd7dd..ccf897f 100644 --- a/frontend/src/components/Topbar.tsx +++ b/frontend/src/components/Topbar.tsx @@ -1,5 +1,5 @@ import { Menu, X } from 'lucide-react'; -import { login, logout } from "../utils/endpoints.ts"; +import { login, logout } from "../utils/api/users.ts"; import { Dropdown, DropdownItem } from "./Dropdown.tsx"; import { GoogleLogin } from '@react-oauth/google'; diff --git a/frontend/src/components/video/VideoCard.tsx b/frontend/src/components/video/VideoCard.tsx index 1561ceb..5653e7b 100644 --- a/frontend/src/components/video/VideoCard.tsx +++ b/frontend/src/components/video/VideoCard.tsx @@ -2,7 +2,7 @@ import clsx from "clsx"; import { formatTime, stringToDate, dateToTimeAgo } from "../../utils/utils.ts"; import { Link } from "react-router-dom"; import { useEffect, useRef, useState } from "react"; -import { isThumbnailAvailable } from "../../utils/endpoints.ts"; +import { isThumbnailAvailable } from "../../utils/api/client.ts"; import { MoreVertical, Pencil, Trash2 } from "lucide-react"; type VideoCardProps = { diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index d21a82e..6ba6821 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -4,7 +4,8 @@ import Topbar from '../components/Topbar' import { Outlet } from 'react-router-dom'; import {useEffect, useState} from "react"; import type {User} from "../utils/types"; -import { getUser, getStreamStatus } from "../utils/endpoints"; +import { getUser } from "../utils/api/users"; +import { getStreamStatus } from "../utils/api/stream"; const MainLayout = () => { const [sidebarToggled, setSidebarToggled] = useState(false); diff --git a/frontend/src/pages/ClipEdit.tsx b/frontend/src/pages/ClipEdit.tsx index be878ca..cc0f4e4 100644 --- a/frontend/src/pages/ClipEdit.tsx +++ b/frontend/src/pages/ClipEdit.tsx @@ -5,7 +5,8 @@ import PlaybackSlider from "./../components/video/PlaybackSlider"; import ClipRangeSlider from "./../components/video/ClipRangeSlider"; import ConfigBox from "../components/video/ConfigBox.tsx"; import ExportWidget from "../components/video/ExportWidget.tsx"; -import { compress, getJob } from "../utils/endpoints" +import { compress } from "../utils/api/media" +import { getJob } from "../utils/api/jobs" import type { VideoMetadata } from "../utils/types.ts"; import Box from "../components/Box.tsx"; import MetadataBox from "../components/video/MetadataBox.tsx"; diff --git a/frontend/src/pages/EditClip.tsx b/frontend/src/pages/EditClip.tsx index 58d86b1..dc5f8d2 100644 --- a/frontend/src/pages/EditClip.tsx +++ b/frontend/src/pages/EditClip.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowLeft, Check } from "lucide-react"; -import { getClipById, patchClip } from "../utils/endpoints.ts"; +import { getClipById, patchClip } from "../utils/api/clips.ts"; import Box from "../components/Box.tsx"; import BlueButton from "../components/buttons/BlueButton.tsx"; diff --git a/frontend/src/pages/EditVod.tsx b/frontend/src/pages/EditVod.tsx index 45b895d..89c1cd9 100644 --- a/frontend/src/pages/EditVod.tsx +++ b/frontend/src/pages/EditVod.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowLeft, Check } from "lucide-react"; -import { getVodById, patchVod } from "../utils/endpoints.ts"; +import { getVodById, patchVod } from "../utils/api/vods.ts"; import Box from "../components/Box.tsx"; import BlueButton from "../components/buttons/BlueButton.tsx"; diff --git a/frontend/src/pages/MyClips.tsx b/frontend/src/pages/MyClips.tsx index bf4f502..5b95f37 100644 --- a/frontend/src/pages/MyClips.tsx +++ b/frontend/src/pages/MyClips.tsx @@ -1,7 +1,7 @@ import VideoCard from "../components/video/VideoCard"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { getClips, deleteClip } from "../utils/endpoints"; +import { getClips, deleteClip } from "../utils/api/clips"; import type { Clip } from "../utils/types"; const MyClips = () => { diff --git a/frontend/src/pages/MyVods.tsx b/frontend/src/pages/MyVods.tsx index 8fe1325..54a0a70 100644 --- a/frontend/src/pages/MyVods.tsx +++ b/frontend/src/pages/MyVods.tsx @@ -1,7 +1,7 @@ import VideoCard from "../components/video/VideoCard"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { getVods, deleteVod } from "../utils/endpoints"; +import { getVods, deleteVod } from "../utils/api/vods"; import type { Vod } from "../utils/types"; const MyVods = () => { diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 1bb946b..156a560 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { Eye, EyeOff, Copy, Check } from "lucide-react"; import Box from "../components/Box.tsx"; -import { getUser } from "../utils/endpoints.ts"; +import { getUser } from "../utils/api/users.ts"; import type { User } from "../utils/types.ts"; import { formatLocalDate } from "../utils/utils.ts"; diff --git a/frontend/src/pages/StreamControl.tsx b/frontend/src/pages/StreamControl.tsx index 4745f1b..480ee20 100644 --- a/frontend/src/pages/StreamControl.tsx +++ b/frontend/src/pages/StreamControl.tsx @@ -3,17 +3,11 @@ import { Radio, Check, Bookmark, Scissors, Film, Activity, Loader2, X } from "lu import clsx from "clsx"; import Box from "../components/Box.tsx"; import BlueButton from "../components/buttons/BlueButton.tsx"; -import { - getUser, - getCurrentStream, - getStreamHistory, - getMarkers, - createMarker, - deleteMarker, - saveSectionByMarkers, - clipSection, - getJob, -} from "../utils/endpoints.ts"; +import { getUser } from "../utils/api/users.ts"; +import { getCurrentStream, getStreamHistory } from "../utils/api/stream.ts"; +import { getMarkers, createMarker, deleteMarker } from "../utils/api/markers.ts"; +import { saveSectionByMarkers, clipSection } from "../utils/api/media.ts"; +import { getJob } from "../utils/api/jobs.ts"; import type { User, StreamStatus, StreamHistoryItem, Marker, JobResponse } from "../utils/types.ts"; import { formatTime, formatLocalDate, stringToDate } from "../utils/utils.ts"; @@ -134,7 +128,6 @@ const StreamControl = () => { refreshMarkers(); const interval = setInterval(refreshMarkers, 5000); return () => clearInterval(interval); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isStreaming, streamStatus?.id]); useEffect(() => { diff --git a/frontend/src/pages/VideoPlayer.tsx b/frontend/src/pages/VideoPlayer.tsx index d91f3eb..436edb4 100644 --- a/frontend/src/pages/VideoPlayer.tsx +++ b/frontend/src/pages/VideoPlayer.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowLeft, Lock, Maximize, Minimize, Pause, Play, Volume1, Volume2, VolumeX } from "lucide-react"; import type { Clip } from "../utils/types"; -import { AuthError, getClipById, getVideoBlob } from "../utils/endpoints.ts"; +import { AuthError } from "../utils/api/client.ts"; +import { getClipById, getVideoBlob } from "../utils/api/clips.ts"; import Box from "../components/Box.tsx"; import { dateToTimeAgo, formatTime, stringToDate } from "../utils/utils.ts"; diff --git a/frontend/src/pages/VodPlayer.tsx b/frontend/src/pages/VodPlayer.tsx index 409542d..22c703b 100644 --- a/frontend/src/pages/VodPlayer.tsx +++ b/frontend/src/pages/VodPlayer.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowLeft, Lock, Maximize, Minimize, Pause, Play, Volume1, Volume2, VolumeX } from "lucide-react"; import type { Vod } from "../utils/types"; -import { AuthError, getVodById, getVodBlob } from "../utils/endpoints.ts"; +import { AuthError } from "../utils/api/client.ts"; +import { getVodById, getVodBlob } from "../utils/api/vods.ts"; import Box from "../components/Box.tsx"; import { dateToTimeAgo, formatTime, stringToDate } from "../utils/utils.ts"; diff --git a/frontend/src/utils/api/client.ts b/frontend/src/utils/api/client.ts new file mode 100644 index 0000000..c4361d8 --- /dev/null +++ b/frontend/src/utils/api/client.ts @@ -0,0 +1,12 @@ +const API_URL = import.meta.env.VITE_API_URL; + +class AuthError extends Error { + constructor() { super("Not authenticated"); this.name = "AuthError"; } +} + +const isThumbnailAvailable = async (thumbnailUrl: string): Promise => { + const response = await fetch(thumbnailUrl, { credentials: 'include' }); + return response.ok; +}; + +export { API_URL, AuthError, isThumbnailAvailable }; diff --git a/frontend/src/utils/api/clips.ts b/frontend/src/utils/api/clips.ts new file mode 100644 index 0000000..2259659 --- /dev/null +++ b/frontend/src/utils/api/clips.ts @@ -0,0 +1,67 @@ +import type { APIResponse, Clip } from "../types.ts"; +import { API_URL, AuthError } from "./client.ts"; + +const getClips = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include' }); + + if (!response.ok) { + const errorResult: APIResponse = await response.json(); + throw new Error(`Failed to fetch clips: ${errorResult.message}`); + } + + const result: APIResponse = await response.json(); + return result.data; +}; + +const getClipById = async (id: string): Promise => { + const response = await fetch(API_URL + `/api/v1/clips/${id}`, { credentials: 'include' }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) throw new AuthError(); + throw new Error(`Failed to fetch clip: ${response.status}`); + } + + const result: APIResponse = await response.json(); + return result.data; +}; + +const getVideoBlob = async (id: string): Promise => { + const response = await fetch(API_URL + `/api/v1/clips/${id}/media`, { credentials: 'include' }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) throw new AuthError(); + throw new Error(`Failed to fetch video ${id}: ${response.status}`); + } + + return response.blob(); +}; + +const patchClip = async (id: number, data: { title?: string; description?: string }): Promise => { + const response = await fetch(API_URL + `/api/v1/clips/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(data), + }); + + if (!response.ok) throw new Error(`Failed to update clip: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to update clip: ${result.message}`); + + return result.data; +}; + +const deleteClip = async (id: number): Promise => { + const response = await fetch(API_URL + `/api/v1/clips/${id}`, { + method: 'DELETE', + credentials: 'include', + }); + + if (!response.ok) throw new Error(`Failed to delete clip: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to delete clip: ${result.message}`); +}; + +export { getClips, getClipById, getVideoBlob, patchClip, deleteClip }; diff --git a/frontend/src/utils/api/jobs.ts b/frontend/src/utils/api/jobs.ts new file mode 100644 index 0000000..5f3bfe4 --- /dev/null +++ b/frontend/src/utils/api/jobs.ts @@ -0,0 +1,15 @@ +import type { APIResponse, JobResponse } from "../types.ts"; +import { API_URL } from "./client.ts"; + +const getJob = async (uuid: string): Promise => { + const response = await fetch(API_URL + `/api/v1/jobs/${uuid}`, { credentials: 'include' }); + + if (!response.ok) throw new Error(`Failed to fetch job: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to fetch job: ${result.message}`); + + return result.data; +}; + +export { getJob }; diff --git a/frontend/src/utils/api/markers.ts b/frontend/src/utils/api/markers.ts new file mode 100644 index 0000000..8eadd80 --- /dev/null +++ b/frontend/src/utils/api/markers.ts @@ -0,0 +1,40 @@ +import type { APIResponse, Marker } from "../types.ts"; +import { API_URL } from "./client.ts"; + +const getMarkers = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/markers', { credentials: 'include' }); + if (!response.ok) throw new Error(`Failed to fetch markers: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to fetch markers: ${result.message}`); + + return result.data; +}; + +const createMarker = async (message: string): Promise => { + const response = await fetch(API_URL + '/api/v1/markers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ message }), + }); + + const result: APIResponse = await response.json(); + if (!response.ok || result.status === 'error') throw new Error(`Failed to add marker: ${result.message}`); + + return result.data; +}; + +const deleteMarker = async (id: number): Promise => { + const response = await fetch(API_URL + `/api/v1/markers/${id}`, { + method: 'DELETE', + credentials: 'include', + }); + + if (!response.ok) throw new Error(`Failed to delete marker: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to delete marker: ${result.message}`); +}; + +export { getMarkers, createMarker, deleteMarker }; diff --git a/frontend/src/utils/api/media.ts b/frontend/src/utils/api/media.ts new file mode 100644 index 0000000..5d8626c --- /dev/null +++ b/frontend/src/utils/api/media.ts @@ -0,0 +1,72 @@ +import type { APIResponse, VideoMetadata } from "../types.ts"; +import { API_URL } from "./client.ts"; + +const compress = async (file: File, options: VideoMetadata): Promise => { + const formData = new FormData(); + formData.append('file', file); + formData.append('title', options.title); + if (options.description) formData.append('description', options.description); + formData.append('startPoint', options.startPoint.toString()); + formData.append('duration', options.duration.toString()); + if (options.fps) formData.append('fps', options.fps.toString()); + if (options.width) formData.append('width', options.width.toString()); + if (options.height) formData.append('height', options.height.toString()); + if (options.fileSize) formData.append('fileSize', options.fileSize.toString()); + + const response = await fetch(API_URL + '/api/v1/media/compress', { + method: 'POST', + body: formData, + credentials: 'include', + }); + + if (!response.ok) throw new Error(`Compression failed: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Compression failed: ${result.message}`); + + return result.data.uuid; +}; + +const saveSection = async (startTime: string, endTime: string, title?: string, description?: string): Promise => { + const response = await fetch(API_URL + '/api/v1/media/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ startTime, endTime, title, description }), + }); + + const result: APIResponse = await response.json(); + if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`); + + return result.data.uuid; +}; + +const saveSectionByMarkers = async (startMarkerId: number, endMarkerId: number, title?: string, description?: string): Promise => { + const response = await fetch(API_URL + '/api/v1/media/save/markers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ startMarkerId, endMarkerId, title, description }), + }); + + const result: APIResponse = await response.json(); + if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`); + + return result.data.uuid; +}; + +const clipSection = async (duration: number, title?: string, description?: string): Promise => { + const response = await fetch(API_URL + '/api/v1/media/clip', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ duration, title, description }), + }); + + const result: APIResponse = await response.json(); + if (!response.ok || result.status === 'error') throw new Error(`Failed to clip section: ${result.message}`); + + return result.data.uuid; +}; + +export { compress, saveSection, saveSectionByMarkers, clipSection }; diff --git a/frontend/src/utils/api/stream.ts b/frontend/src/utils/api/stream.ts new file mode 100644 index 0000000..7cf5fc0 --- /dev/null +++ b/frontend/src/utils/api/stream.ts @@ -0,0 +1,31 @@ +import type { APIResponse, StreamHistoryItem, StreamStatus } from "../types.ts"; +import { API_URL } from "./client.ts"; + +const getStreamStatus = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' }); + if (!response.ok) return false; + const result: APIResponse = await response.json(); + return result.status === 'success' && result.data?.isStreaming === true; +}; + +const getCurrentStream = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' }); + if (!response.ok) throw new Error(`Failed to fetch stream status: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to fetch stream status: ${result.message}`); + + return result.data; +}; + +const getStreamHistory = async (userId: number): Promise => { + const response = await fetch(API_URL + `/api/v1/stream/history/${userId}`, { credentials: 'include' }); + if (!response.ok) throw new Error(`Failed to fetch stream history: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to fetch stream history: ${result.message}`); + + return result.data; +}; + +export { getStreamStatus, getCurrentStream, getStreamHistory }; diff --git a/frontend/src/utils/api/users.ts b/frontend/src/utils/api/users.ts new file mode 100644 index 0000000..73f4af1 --- /dev/null +++ b/frontend/src/utils/api/users.ts @@ -0,0 +1,42 @@ +import type { APIResponse, User } from "../types.ts"; +import { API_URL } from "./client.ts"; + +const login = async (googleToken: string): Promise => { + const response = await fetch(API_URL + '/api/v1/users/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: googleToken }), + credentials: 'include', + }); + + if (!response.ok) throw new Error(`Login failed: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Login failed: ${result.message}`); + + return result.data.token; +}; + +const logout = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/users/logout', { + method: 'POST', + credentials: 'include', + }); + + if (!response.ok) throw new Error(`Logout failed: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Logout failed: ${result.message}`); +}; + +const getUser = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/users/me', { credentials: 'include' }); + if (!response.ok) return null; + + const result: APIResponse = await response.json(); + if (result.status === 'error') return null; + + return result.data; +}; + +export { login, logout, getUser }; diff --git a/frontend/src/utils/api/vods.ts b/frontend/src/utils/api/vods.ts new file mode 100644 index 0000000..4f45de3 --- /dev/null +++ b/frontend/src/utils/api/vods.ts @@ -0,0 +1,67 @@ +import type { APIResponse, Vod } from "../types.ts"; +import { API_URL, AuthError } from "./client.ts"; + +const getVods = async (): Promise => { + const response = await fetch(API_URL + '/api/v1/vods', { credentials: 'include' }); + + if (!response.ok) { + const errorResult: APIResponse = await response.json(); + throw new Error(`Failed to fetch vods: ${errorResult.message}`); + } + + const result: APIResponse = await response.json(); + return result.data; +}; + +const getVodById = async (id: string): Promise => { + const response = await fetch(API_URL + `/api/v1/vods/${id}`, { credentials: 'include' }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) throw new AuthError(); + throw new Error(`Failed to fetch vod: ${response.status}`); + } + + const result: APIResponse = await response.json(); + return result.data; +}; + +const getVodBlob = async (id: string): Promise => { + const response = await fetch(API_URL + `/api/v1/vods/${id}/media`, { credentials: 'include' }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) throw new AuthError(); + throw new Error(`Failed to fetch vod ${id}: ${response.status}`); + } + + return response.blob(); +}; + +const patchVod = async (id: number, data: { title?: string; description?: string }): Promise => { + const response = await fetch(API_URL + `/api/v1/vods/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(data), + }); + + if (!response.ok) throw new Error(`Failed to update vod: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to update vod: ${result.message}`); + + return result.data; +}; + +const deleteVod = async (id: number): Promise => { + const response = await fetch(API_URL + `/api/v1/vods/${id}`, { + method: 'DELETE', + credentials: 'include', + }); + + if (!response.ok) throw new Error(`Failed to delete vod: ${response.status}`); + + const result: APIResponse = await response.json(); + if (result.status === 'error') throw new Error(`Failed to delete vod: ${result.message}`); +}; + +export { getVods, getVodById, getVodBlob, patchVod, deleteVod }; diff --git a/frontend/src/utils/endpoints.ts b/frontend/src/utils/endpoints.ts deleted file mode 100644 index 3fac59b..0000000 --- a/frontend/src/utils/endpoints.ts +++ /dev/null @@ -1,346 +0,0 @@ -import type { VideoMetadata, APIResponse, User, Clip, Vod, JobResponse, StreamStatus, StreamHistoryItem, Marker } from "./types.ts"; - -const API_URL = import.meta.env.VITE_API_URL; - -export class AuthError extends Error { - constructor() { super("Not authenticated"); this.name = "AuthError"; } -} - -const login = async (googleToken: string): Promise => { - const response = await fetch(API_URL + '/api/v1/users/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token: googleToken }), - credentials: 'include', - }); - - if (!response.ok) throw new Error(`Login failed: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Login failed: ${result.message}`); - - return result.data.token; -}; - -const logout = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/users/logout', { - method: 'POST', - credentials: 'include', - }); - - if (!response.ok) throw new Error(`Logout failed: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Logout failed: ${result.message}`); -}; - -const compress = async (file: File, options: VideoMetadata): Promise => { - const formData = new FormData(); - formData.append('file', file); - formData.append('title', options.title); - if (options.description) formData.append('description', options.description); - formData.append('startPoint', options.startPoint.toString()); - formData.append('duration', options.duration.toString()); - if (options.fps) formData.append('fps', options.fps.toString()); - if (options.width) formData.append('width', options.width.toString()); - if (options.height) formData.append('height', options.height.toString()); - if (options.fileSize) formData.append('fileSize', options.fileSize.toString()); - - const response = await fetch(API_URL + '/api/v1/media/compress', { - method: 'POST', - body: formData, - credentials: 'include', - }); - - if (!response.ok) throw new Error(`Compression failed: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Compression failed: ${result.message}`); - - return result.data.uuid; -}; - -const getJob = async (uuid: string): Promise => { - const response = await fetch(API_URL + `/api/v1/jobs/${uuid}`, { credentials: 'include' }); - - if (!response.ok) throw new Error(`Failed to fetch job: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to fetch job: ${result.message}`); - - return result.data; -}; - -const getUser = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/users/me', { credentials: 'include' }); - if (!response.ok) return null; - - const result: APIResponse = await response.json(); - if (result.status === 'error') return null; - - return result.data; -}; - -const getClips = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/clips', { credentials: 'include' }); - - if (!response.ok) { - const errorResult: APIResponse = await response.json(); - throw new Error(`Failed to fetch clips: ${errorResult.message}`); - } - - const result: APIResponse = await response.json(); - return result.data; -}; - -const getClipById = async (id: string): Promise => { - const response = await fetch(API_URL + `/api/v1/clips/${id}`, { credentials: 'include' }); - - if (!response.ok) { - if (response.status === 401 || response.status === 403) throw new AuthError(); - throw new Error(`Failed to fetch clip: ${response.status}`); - } - - const result: APIResponse = await response.json(); - return result.data; -}; - -const getVideoBlob = async (id: string): Promise => { - const response = await fetch(API_URL + `/api/v1/clips/${id}/media`, { credentials: 'include' }); - - if (!response.ok) { - if (response.status === 401 || response.status === 403) throw new AuthError(); - throw new Error(`Failed to fetch video ${id}: ${response.status}`); - } - - return response.blob(); -}; - -const patchClip = async (id: number, data: { title?: string; description?: string }): Promise => { - const response = await fetch(API_URL + `/api/v1/clips/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify(data), - }); - - if (!response.ok) throw new Error(`Failed to update clip: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to update clip: ${result.message}`); - - return result.data; -}; - -const deleteClip = async (id: number): Promise => { - const response = await fetch(API_URL + `/api/v1/clips/${id}`, { - method: 'DELETE', - credentials: 'include', - }); - - if (!response.ok) throw new Error(`Failed to delete clip: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to delete clip: ${result.message}`); -}; - -const getVods = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/vods', { credentials: 'include' }); - - if (!response.ok) { - const errorResult: APIResponse = await response.json(); - throw new Error(`Failed to fetch vods: ${errorResult.message}`); - } - - const result: APIResponse = await response.json(); - return result.data; -}; - -const getVodById = async (id: string): Promise => { - const response = await fetch(API_URL + `/api/v1/vods/${id}`, { credentials: 'include' }); - - if (!response.ok) { - if (response.status === 401 || response.status === 403) throw new AuthError(); - throw new Error(`Failed to fetch vod: ${response.status}`); - } - - const result: APIResponse = await response.json(); - return result.data; -}; - -const getVodBlob = async (id: string): Promise => { - const response = await fetch(API_URL + `/api/v1/vods/${id}/media`, { credentials: 'include' }); - - if (!response.ok) { - if (response.status === 401 || response.status === 403) throw new AuthError(); - throw new Error(`Failed to fetch vod ${id}: ${response.status}`); - } - - return response.blob(); -}; - -const patchVod = async (id: number, data: { title?: string; description?: string }): Promise => { - const response = await fetch(API_URL + `/api/v1/vods/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify(data), - }); - - if (!response.ok) throw new Error(`Failed to update vod: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to update vod: ${result.message}`); - - return result.data; -}; - -const deleteVod = async (id: number): Promise => { - const response = await fetch(API_URL + `/api/v1/vods/${id}`, { - method: 'DELETE', - credentials: 'include', - }); - - if (!response.ok) throw new Error(`Failed to delete vod: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to delete vod: ${result.message}`); -}; - -const getStreamStatus = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' }); - if (!response.ok) return false; - const result: APIResponse = await response.json(); - return result.status === 'success' && result.data?.isStreaming === true; -}; - -const isThumbnailAvailable = async (thumbnailUrl: string): Promise => { - const response = await fetch(thumbnailUrl, { credentials: 'include' }); - return response.ok; -}; - -const getCurrentStream = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/stream/current', { credentials: 'include' }); - if (!response.ok) throw new Error(`Failed to fetch stream status: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to fetch stream status: ${result.message}`); - - return result.data; -}; - -const getStreamHistory = async (userId: number): Promise => { - const response = await fetch(API_URL + `/api/v1/stream/history/${userId}`, { credentials: 'include' }); - if (!response.ok) throw new Error(`Failed to fetch stream history: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to fetch stream history: ${result.message}`); - - return result.data; -}; - -const getMarkers = async (): Promise => { - const response = await fetch(API_URL + '/api/v1/markers', { credentials: 'include' }); - if (!response.ok) throw new Error(`Failed to fetch markers: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to fetch markers: ${result.message}`); - - return result.data; -}; - -const createMarker = async (message: string): Promise => { - const response = await fetch(API_URL + '/api/v1/markers', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ message }), - }); - - const result: APIResponse = await response.json(); - if (!response.ok || result.status === 'error') throw new Error(`Failed to add marker: ${result.message}`); - - return result.data; -}; - -const deleteMarker = async (id: number): Promise => { - const response = await fetch(API_URL + `/api/v1/markers/${id}`, { - method: 'DELETE', - credentials: 'include', - }); - - if (!response.ok) throw new Error(`Failed to delete marker: ${response.status}`); - - const result: APIResponse = await response.json(); - if (result.status === 'error') throw new Error(`Failed to delete marker: ${result.message}`); -}; - -const saveSection = async (startTime: string, endTime: string, title?: string, description?: string): Promise => { - const response = await fetch(API_URL + '/api/v1/media/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ startTime, endTime, title, description }), - }); - - const result: APIResponse = await response.json(); - if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`); - - return result.data.uuid; -}; - -const saveSectionByMarkers = async (startMarkerId: number, endMarkerId: number, title?: string, description?: string): Promise => { - const response = await fetch(API_URL + '/api/v1/media/save/markers', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ startMarkerId, endMarkerId, title, description }), - }); - - const result: APIResponse = await response.json(); - if (!response.ok || result.status === 'error') throw new Error(`Failed to save section: ${result.message}`); - - return result.data.uuid; -}; - -const clipSection = async (duration: number, title?: string, description?: string): Promise => { - const response = await fetch(API_URL + '/api/v1/media/clip', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ duration, title, description }), - }); - - const result: APIResponse = await response.json(); - if (!response.ok || result.status === 'error') throw new Error(`Failed to clip section: ${result.message}`); - - return result.data.uuid; -}; - -export { - login, - logout, - compress, - getJob, - getUser, - getClips, - getClipById, - getVideoBlob, - patchClip, - deleteClip, - getVods, - getVodById, - getVodBlob, - patchVod, - deleteVod, - getStreamStatus, - isThumbnailAvailable, - getCurrentStream, - getStreamHistory, - getMarkers, - createMarker, - deleteMarker, - saveSection, - saveSectionByMarkers, - clipSection, -};