Files
FlightTube/src/api.ts
T
Vincent Rozenberg 22e31afc1d feat: remove every subscription from Settings, and reopen the last video
Remove all sits beside Import in Settings, since emptying the list is
the other half of replacing it. It asks first and counts what goes:
210 channels, 3339 videos, 50 downloaded files, in those words. Nothing
changes on YouTube, and the confirmation says so — importing or reading
the list again brings it all back.

It takes the same path an import does when nothing survives, so a video
saved on its own from the menu bar is left alone: its channel was never
a subscription, and this is about subscriptions.

The app also reopens whatever was playing when it last closed, if that
video is still in the feed and still playable — offline, one that was
streaming is not. The note is made while the player is open and dropped
when it is closed, so a restart only reopens something you were in the
middle of, never something you had deliberately finished with. Verified
both ways: quitting mid-video came back to it, closing the player first
came back to the feed.

The panel's Remove all downloads is gone; it was a misreading of the
request.
2026-09-04 02:03:46 +02:00

196 lines
7.4 KiB
TypeScript

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<Prereqs>("check_prereqs");
export const listChannels = () => invoke<ChannelWithCount[]>("list_channels");
export const listFeed = (filter: FeedFilter) =>
invoke<FeedItem[]>("list_feed", { filter });
export const refreshFeeds = () => invoke<RefreshSummary>("refresh_feeds");
export const downloadVideo = (videoId: string, quality: Quality, subLang: string) =>
invoke<void>("download_video", { videoId, quality, subLang });
/** Downloads still unfinished from a previous run, as (id, quality, subLang). */
export const interruptedDownloads = () =>
invoke<Array<[string, string, string]>>("interrupted_downloads");
/** Stops everything downloading or waiting to. Returns how many were stopped. */
export const cancelAllDownloads = () => invoke<number>("cancel_all_downloads");
export const deleteAllDownloads = () => invoke<number>("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<number>("fetch_durations", { visible });
/** Browsers installed here that yt-dlp can read cookies from: [id, label]. */
export const listBrowsers = () => invoke<Array<[string, string]>>("list_browsers");
/** Empty string signs out. */
export const setCookieSource = (browser: string) =>
invoke<void>("set_cookie_source", { browser });
/** Resolves a known video to check whether YouTube is currently reachable. */
export const testYoutube = () => invoke<string>("test_youtube");
export const checkYtDlpUpdate = () => invoke<UpdateStatus>("check_yt_dlp_update");
/** Downloads the newest yt-dlp and switches to it. Returns its version. */
export const updateYtDlp = () => invoke<string>("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<ScrapeResult>("scrape_subscriptions");
export const previewScrapedImport = (channels: ScrapeResult["channels"]) =>
invoke<ImportPreview>("preview_scraped_import", { channels });
export const importScraped = (channels: ScrapeResult["channels"]) =>
invoke<number>("import_scraped", { channels });
/** What emptying the subscription list would take with it. */
export const previewRemoveAllSubscriptions = () =>
invoke<ImportPreview>("preview_remove_all_subscriptions");
export const removeAllSubscriptions = () => invoke<number>("remove_all_subscriptions");
/** The menu bar panel's own actions. */
export const traySaveVideo = () => invoke<void>("tray_save_video");
export const showMainWindow = () => invoke<void>("show_main_window");
export const hidePanel = () => invoke<void>("hide_panel");
export const quitApp = () => invoke<void>("quit_app");
/** Adds one channel from any YouTube link. Returns its title. */
export const addChannel = (url: string) => invoke<string>("add_channel", { url });
export interface RemovalPreview {
title: string;
videos: number;
downloaded: number;
}
export const previewDeleteChannel = (channelId: string) =>
invoke<RemovalPreview>("preview_delete_channel", { channelId });
/** Unsubscribes in the app only — nothing changes on YouTube. */
export const deleteChannel = (channelId: string) =>
invoke<void>("delete_channel", { channelId });
/** Keeps the menu bar's downloads matching the window's settings. */
export const setDownloadDefaults = (quality: string, subLang: string) =>
invoke<void>("set_download_defaults", { quality, subLang });
export const listSubtitles = (videoId: string) =>
invoke<Array<[string, string]>>("list_subtitles", { videoId });
/** Subtitles read out of a downloaded file, as (language, WebVTT text). */
export const embeddedSubtitles = (videoId: string) =>
invoke<Array<[string, string]>>("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<Array<[string, string]>>("fetch_subtitles", { videoId, lang });
export const savePlayback = (videoId: string, position: number, duration: number) =>
invoke<void>("save_playback", { videoId, position, duration });
export const cancelDownload = (videoId: string) =>
invoke<void>("cancel_download", { videoId });
export const deleteDownload = (videoId: string) =>
invoke<void>("delete_download", { videoId });
export const getConnectivity = () => invoke<boolean>("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<string> {
const s = await invoke<Stream>("resolve_stream", { videoId, maxHeight });
if (!s.url) throw new Error("No playable stream was returned.");
return s.url;
}
export const setLibraryPath = (path: string) =>
invoke<string>("set_library_path", { path });
export const openExternal = (url: string) =>
invoke<void>("open_external", { url });
/** Opens the native file picker for a Takeout subscriptions.csv. */
export async function pickTakeoutFile(): Promise<string | null> {
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<ImportPreview>("preview_takeout_import", { path });
/** Replaces the whole subscription list. Confirm with the user first. */
export const importTakeoutCsv = (path: string) =>
invoke<number>("import_takeout_csv", { path });
export async function pickLibraryFolder(): Promise<string | null> {
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<RefreshProgress>("refresh:progress", (e) => cb(e.payload));
export const onDownloadProgress = (cb: (p: DownloadProgressEvent) => void) =>
listen<DownloadProgressEvent>("download:progress", (e) => cb(e.payload));
export const onDownloadState = (cb: (p: DownloadStateEvent) => void) =>
listen<DownloadStateEvent>("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 : "";
};