// Playlists: local + YouTube-linked, item add/remove/reorder, the built-in Watch later shortcut, // and the push-to-YouTube plan/apply flow. Only touches core's req. import { req } from "./core"; import type { Video } from "./feed"; // Not exported: consumers compare `playlist.kind`/`.source` against string literals, they never // name these types — keeping them local satisfies the no-unused-exports gate (knip). type PlaylistKind = "user" | "watch_later"; type PlaylistSource = "local" | "youtube"; export interface Playlist { id: number; name: string; kind: PlaylistKind; source: PlaylistSource; yt_playlist_id: string | null; dirty: boolean; // linked local playlist has edits not yet pushed to YouTube item_count: number; total_duration_seconds: number; cover_thumbnail: string | null; has_video?: boolean; // only set when listed with ?contains= } export interface PlaylistDetail { id: number; name: string; kind: PlaylistKind; source: PlaylistSource; yt_playlist_id: string | null; dirty: boolean; items: Video[]; } // Two-way sync plan: what each side gains/loses under the 3-way merge (neither side wins // wholesale). Local edits are free; only the YouTube writes cost quota. export interface PushPlan { action: "create" | "update"; add_yt: number; // local-adds to push out to YouTube add_local: number; // YouTube-adds to pull into Siftlode remove_yt: number; // genuine local deletes propagated to YouTube remove_local: number; // genuine YouTube deletes propagated to Siftlode to_reorder: number; rename: boolean; units_estimate: number; remaining_today: number; affordable: boolean; } export interface PushResult { created: number; inserted: number; // added on YouTube deleted: number; // removed on YouTube reordered: number; pulled: number; // added into Siftlode from YouTube removed_local: number; // removed from Siftlode (deleted on YouTube) failures: string[]; yt_playlist_id: string | null; } export const playlistsApi = { playlists: (containsVideoId?: string): Promise => req(`/api/playlists${containsVideoId ? `?contains=${containsVideoId}` : ""}`), playlist: (id: number): Promise => req(`/api/playlists/${id}`), createPlaylist: (name: string): Promise => req("/api/playlists", { method: "POST", body: JSON.stringify({ name }) }), renamePlaylist: (id: number, name: string): Promise => req(`/api/playlists/${id}`, { method: "PATCH", body: JSON.stringify({ name }) }), deletePlaylist: (id: number, onYoutube = false) => req(`/api/playlists/${id}${onYoutube ? "?on_youtube=true" : ""}`, { method: "DELETE" }), addToPlaylist: (id: number, videoId: string) => req(`/api/playlists/${id}/items`, { method: "POST", body: JSON.stringify({ video_id: videoId }), }), removeFromPlaylist: (id: number, videoId: string) => req(`/api/playlists/${id}/items/${videoId}`, { method: "DELETE" }), reorderPlaylist: (id: number, videoIds: string[]) => req(`/api/playlists/${id}/order`, { method: "PUT", body: JSON.stringify({ video_ids: videoIds }), }), // Bookmark shortcut for the built-in Watch later playlist. watchLaterAdd: (videoId: string) => req("/api/playlists/watch-later", { method: "POST", body: JSON.stringify({ video_id: videoId }), }), watchLaterRemove: (videoId: string) => req(`/api/playlists/watch-later/${videoId}`, { method: "DELETE" }), syncYoutubePlaylists: (): Promise<{ synced: number; reason?: string }> => req("/api/playlists/sync-youtube", { method: "POST" }), revertPlaylist: (id: number): Promise<{ items: number }> => req(`/api/playlists/${id}/revert-youtube`, { method: "POST" }), playlistPushPlan: (id: number): Promise => req(`/api/playlists/${id}/push-plan`), pushPlaylist: (id: number): Promise => req(`/api/playlists/${id}/push`, { method: "POST" }), };