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.
196 lines
7.4 KiB
TypeScript
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 : "";
|
|
};
|