import { api } from './client' import type { Media, PlaybackSegmentsResponse, Playlist } from '../types' // History rows arrive joined with their Media row; the backend returns null // for orphaned rows whose media has been removed. export interface HistoryItem { id: string user_id: string media_id: string position_ms: number duration_ms: number watched_at: string completed: boolean media?: Media | null created_at: string updated_at: string } export interface PlaylistDetail { playlist: Playlist items: Media[] } export interface ExternalPlayer { name: string scheme: string url: string } export interface PlaybackProgressRequest { media_id: string position_ms: number duration_ms: number session_id?: string session_started_at_ms?: number sequence?: number } function publicOriginHeader() { if (typeof window === 'undefined' || !window.location?.origin) return undefined return { 'X-MeBox-Public-Origin': window.location.origin } } export const playbackAPI = { getResume: (mediaId: string, signal?: AbortSignal) => api .get<{ position_ms: number; duration_ms: number; completed: boolean }>(`/playback/${encodeURIComponent(mediaId)}/resume`, { signal }) .then((r) => r.data), // 片头/片尾片段:播放开始后再调用,服务端可能需要几秒去外部数据库取数, // 因此绝不能让它挡在起播路径上。 segments: (mediaId: string) => api .get(`/playback/${encodeURIComponent(mediaId)}/segments`) .then((r) => r.data), // 手动标记:把当前位置记为片头结束 / 片尾开始(clear 则清除)。服务端会按季 // 传播到同季其它尚未标记的集,并返回合并后的完整片段列表。 markSegment: ( mediaId: string, payload: { kind: 'intro' | 'credits'; position_ms?: number; clear?: boolean }, ) => api .put(`/playback/${encodeURIComponent(mediaId)}/segments`, payload) .then((r) => r.data), recordProgress: (payload: PlaybackProgressRequest) => api.post('/history', payload).then((r) => r.data), recentHistory: () => api.get<{ items: HistoryItem[] }>('/history').then((r) => r.data.items), toggleFavourite: (mediaId: string) => api .post<{ favourite: boolean }>(`/favourites/${mediaId}`) .then((r) => r.data.favourite), listFavourites: () => api.get<{ items: Media[] }>('/favourites').then((r) => r.data.items), listFavouriteIDs: () => api.get<{ ids: string[] }>('/favourites', { params: { ids: 1 } }).then((r) => r.data.ids ?? []), favouriteStatus: (mediaId: string) => api .get<{ favourite: boolean }>(`/media/${encodeURIComponent(mediaId)}/favorite/status`) .then((r) => r.data.favourite), listPlaylists: () => api.get<{ items: Playlist[] }>('/playlists').then((r) => r.data.items), createPlaylist: (name: string, isPublic = false) => api .post('/playlists', { name, is_public: isPublic }) .then((r) => r.data), getPlaylist: (id: string) => api.get(`/playlists/${id}`).then((r) => r.data), addToPlaylist: (id: string, mediaId: string) => api.post(`/playlists/${id}/items`, { media_id: mediaId }).then((r) => r.data), removeFromPlaylist: (id: string, mediaId: string) => api.delete(`/playlists/${id}/items/${mediaId}`).then((r) => r.data), deletePlaylist: (id: string) => api.delete(`/playlists/${id}`).then((r) => r.data), externalPlayers: (mediaId: string) => api .get<{ players: ExternalPlayer[]; url?: string }>(`/playback/${mediaId}/external-players`, { headers: publicOriginHeader(), }) .then((r) => r.data), externalURL: (mediaId: string) => api .get<{ url: string }>(`/playback/${mediaId}/external-url`, { headers: publicOriginHeader(), }) .then((r) => r.data), }