const AUTH_ERROR_MESSAGES: Record = { google_error: 'Google вернул ошибку авторизации', invalid_state: 'Сессия авторизации устарела, попробуйте снова', exchange_failed: 'Не удалось обменять код авторизации', userinfo_failed: 'Не удалось получить данные аккаунта Google', account_not_allowed: 'Этот Google-аккаунт не разрешён для входа', } export function authErrorMessage(code: string): string { return AUTH_ERROR_MESSAGES[code] ?? 'Неизвестная ошибка авторизации' } async function request(path: string, options: RequestInit = {}): Promise { const res = await fetch(path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) }, ...options, }) if (!res.ok) { let detail = `${res.status}` try { const body = await res.json() detail = body.detail ?? detail } catch { // ignore } throw new Error(detail) } return res.json() } export interface AuthStatus { authenticated: boolean connected: boolean email: string | null } export function getAuthStatus() { return request('/api/auth/status') } export function startGoogleAuth() { window.location.href = '/api/auth/google/start' } export function logout() { return request<{ ok: boolean }>('/api/auth/logout', { method: 'POST' }) } export interface ChannelDto { id: number youtube_channel_id: string title: string description: string | null thumbnail_url: string | null uploads_playlist_id: string | null subscribed: boolean last_synced_at: string | null category_ids: number[] } export function getChannel(channelId: number) { return request(`/api/channels/${channelId}`) } export function listChannels( params: { search?: string; categoryId?: number; uncategorized?: boolean } = {}, ) { const qs = new URLSearchParams() if (params.search) qs.set('search', params.search) if (params.categoryId != null) qs.set('category_id', String(params.categoryId)) if (params.uncategorized) qs.set('uncategorized', 'true') const suffix = qs.toString() ? `?${qs.toString()}` : '' return request(`/api/channels${suffix}`) } export function setChannelCategories(channelId: number, categoryIds: number[]) { return request(`/api/channels/${channelId}/categories`, { method: 'PUT', body: JSON.stringify({ category_ids: categoryIds }), }) } export interface CategoryDto { id: number name: string slug: string sort_order: number channel_count: number } export function listCategories() { return request('/api/categories') } export function createCategory(name: string) { return request('/api/categories', { method: 'POST', body: JSON.stringify({ name }), }) } export function updateCategory(id: number, name: string) { return request(`/api/categories/${id}`, { method: 'PATCH', body: JSON.stringify({ name }), }) } export function deleteCategory(id: number) { return fetch(`/api/categories/${id}`, { method: 'DELETE', credentials: 'include' }).then((res) => { if (!res.ok) throw new Error(`${res.status}`) }) } export function reorderCategories(categoryIds: number[]) { return request('/api/categories/reorder', { method: 'POST', body: JSON.stringify({ category_ids: categoryIds }), }) } export interface SyncStatus { status: string started_at?: string finished_at?: string | null error?: string | null running: boolean [key: string]: unknown } export function getSyncStatus() { return request<{ subscriptions: SyncStatus; videos: SyncStatus }>('/api/sync/status') } export function syncSubscriptions() { return request('/api/sync/subscriptions', { method: 'POST' }) } export function syncVideos() { return request('/api/sync/videos', { method: 'POST' }) } export interface FeedVideoDto { youtube_video_id: string title: string description: string | null channel: { id: number youtube_channel_id: string title: string thumbnail_url: string | null } thumbnail_url: string | null published_at: string duration_seconds: number | null youtube_url: string categories: { id: number; name: string }[] local: { available: boolean status: string progress_percent: number | null media_url: string | null } } export function getVideo(youtubeVideoId: string) { return request(`/api/videos/${youtubeVideoId}`) } export interface DownloadJobDto { status: string progress_percent: number | null media_url: string | null error_message: string | null requested_at?: string started_at?: string | null completed_at?: string | null } export function downloadVideo(youtubeVideoId: string) { return request(`/api/videos/${youtubeVideoId}/download`, { method: 'POST' }) } export function getDownloadStatus(youtubeVideoId: string) { return request(`/api/videos/${youtubeVideoId}/download-status`) } export function recheckLocal(youtubeVideoId: string) { return request(`/api/videos/${youtubeVideoId}/recheck-local`, { method: 'POST' }) } export function getFeed( params: { categoryId?: number; uncategorized?: boolean; channelId?: number; cursor?: string } = {}, ) { const qs = new URLSearchParams() if (params.categoryId != null) qs.set('category_id', String(params.categoryId)) if (params.uncategorized) qs.set('uncategorized', 'true') if (params.channelId != null) qs.set('channel_id', String(params.channelId)) if (params.cursor) qs.set('cursor', params.cursor) const suffix = qs.toString() ? `?${qs.toString()}` : '' return request<{ items: FeedVideoDto[]; next_cursor: string | null }>(`/api/feed${suffix}`) }