import { invoke, convertFileSrc } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { open } from "@tauri-apps/plugin-dialog"; import type { ChannelWithCount, DownloadProgressEvent, DownloadStateEvent, FeedFilter, FeedItem, ImportPreview, Prereqs, Quality, RefreshProgress, RefreshSummary, Stream, UpdateStatus, } from "./types"; export const checkPrereqs = () => invoke("check_prereqs"); export const listChannels = () => invoke("list_channels"); export const listFeed = (filter: FeedFilter) => invoke("list_feed", { filter }); export const refreshFeeds = () => invoke("refresh_feeds"); export const downloadVideo = (videoId: string, quality: Quality, subLang: string) => invoke("download_video", { videoId, quality, subLang }); /** Downloads still unfinished from a previous run, as (id, quality, subLang). */ export const interruptedDownloads = () => invoke>("interrupted_downloads"); /** Stops everything downloading or waiting to. Returns how many were stopped. */ export const cancelAllDownloads = () => invoke("cancel_all_downloads"); export const deleteAllDownloads = () => invoke("delete_all_downloads"); /** Fills in missing video lengths, a batch at a time. Returns how many. */ /** Fills lengths for the videos on screen first. */ export const fetchDurations = (visible: string[]) => invoke("fetch_durations", { visible }); /** Browsers installed here that yt-dlp can read cookies from: [id, label]. */ export const listBrowsers = () => invoke>("list_browsers"); /** Empty string signs out. */ export const setCookieSource = (browser: string) => invoke("set_cookie_source", { browser }); /** Resolves a known video to check whether YouTube is currently reachable. */ export const testYoutube = () => invoke("test_youtube"); export const checkYtDlpUpdate = () => invoke("check_yt_dlp_update"); /** Downloads the newest yt-dlp and switches to it. Returns its version. */ export const updateYtDlp = () => invoke("update_yt_dlp"); /** WebVTT sidecars beside a downloaded video, as [language, path] pairs. */ export interface ScrapeResult { channels: Array<{ id: string; title: string; url: string }>; unresolved: string[]; looked_up: number; } /** Reads the subscription list out of a signed-in browser page. */ export const scrapeSubscriptions = () => invoke("scrape_subscriptions"); export const previewScrapedImport = (channels: ScrapeResult["channels"]) => invoke("preview_scraped_import", { channels }); export const importScraped = (channels: ScrapeResult["channels"]) => invoke("import_scraped", { channels }); /** What emptying the subscription list would take with it. */ export const previewRemoveAllSubscriptions = () => invoke("preview_remove_all_subscriptions"); export const removeAllSubscriptions = () => invoke("remove_all_subscriptions"); /** The menu bar panel's own actions. */ export const traySaveVideo = () => invoke("tray_save_video"); export const showMainWindow = () => invoke("show_main_window"); export const hidePanel = () => invoke("hide_panel"); export const quitApp = () => invoke("quit_app"); /** Adds one channel from any YouTube link. Returns its title. */ export const addChannel = (url: string) => invoke("add_channel", { url }); export interface RemovalPreview { title: string; videos: number; downloaded: number; } export const previewDeleteChannel = (channelId: string) => invoke("preview_delete_channel", { channelId }); /** Unsubscribes in the app only — nothing changes on YouTube. */ export const deleteChannel = (channelId: string) => invoke("delete_channel", { channelId }); /** Keeps the menu bar's downloads matching the window's settings. */ export const setDownloadDefaults = (quality: string, subLang: string) => invoke("set_download_defaults", { quality, subLang }); export const listSubtitles = (videoId: string) => invoke>("list_subtitles", { videoId }); /** Subtitles read out of a downloaded file, as (language, WebVTT text). */ export const embeddedSubtitles = (videoId: string) => invoke>("embedded_subtitles", { videoId }); /** Subtitles as (language, WebVTT text) — fetched and cached when there are * none beside the video. Streaming has no other source. */ export const fetchSubtitles = (videoId: string, lang: string) => invoke>("fetch_subtitles", { videoId, lang }); export const savePlayback = (videoId: string, position: number, duration: number) => invoke("save_playback", { videoId, position, duration }); export const cancelDownload = (videoId: string) => invoke("cancel_download", { videoId }); export const deleteDownload = (videoId: string) => invoke("delete_download", { videoId }); export const getConnectivity = () => invoke("get_connectivity"); /** * A directly playable URL for an undownloaded video. When a quality cap is set * the backend serves a rewritten playlist from its own loopback server, so this * is always a plain URL either way. */ export async function resolveStream( videoId: string, maxHeight: number | null, ): Promise { const s = await invoke("resolve_stream", { videoId, maxHeight }); if (!s.url) throw new Error("No playable stream was returned."); return s.url; } export const setLibraryPath = (path: string) => invoke("set_library_path", { path }); export const openExternal = (url: string) => invoke("open_external", { url }); /** Opens the native file picker for a Takeout subscriptions.csv. */ export async function pickTakeoutFile(): Promise { const path = await open({ multiple: false, directory: false, filters: [{ name: "Takeout subscriptions", extensions: ["csv"] }], }); return typeof path === "string" ? path : null; } export const previewTakeoutImport = (path: string) => invoke("preview_takeout_import", { path }); /** Replaces the whole subscription list. Confirm with the user first. */ export const importTakeoutCsv = (path: string) => invoke("import_takeout_csv", { path }); export async function pickLibraryFolder(): Promise { const path = await open({ directory: true, multiple: false }); if (typeof path !== "string") return null; return setLibraryPath(path); } export const onRefreshProgress = (cb: (p: RefreshProgress) => void) => listen("refresh:progress", (e) => cb(e.payload)); export const onDownloadProgress = (cb: (p: DownloadProgressEvent) => void) => listen("download:progress", (e) => cb(e.payload)); export const onDownloadState = (cb: (p: DownloadStateEvent) => void) => listen("download:state", (e) => cb(e.payload)); export type { UnlistenFn }; /** Local file path -> a URL the webview is allowed to load. */ export const fileUrl = (path: string) => convertFileSrc(path); /** * Prefers the on-disk thumbnail so the feed still renders with no network, * falling back to the remote URL for videos not yet cached. */ export const thumbSrc = (item: FeedItem, online: boolean) => { if (item.thumb_path) return convertFileSrc(item.thumb_path); return online ? item.thumb_url : ""; };