This commit is contained in:
2025-10-29 00:58:37 +01:00
parent 73da41b514
commit dd9d076bd2
33 changed files with 1172 additions and 385 deletions

View File

@@ -1,57 +1,104 @@
import Client from "../Client";
export type Choices = {
file_types: string[];
qualities: string[];
export type FormatOption = {
format_id: string;
ext: string | null;
vcodec: string | null;
acodec: string | null;
fps: number | null;
tbr: number | null;
abr: number | null;
vbr: number | null;
asr: number | null;
filesize: number | null;
filesize_approx: number | null;
estimated_size_bytes: number | null;
size_ok: boolean;
format_note: string | null;
resolution: string | null; // e.g. "1920x1080"
audio_only: boolean;
};
export type DownloadPayload = {
url: string;
file_type?: string;
quality?: string;
export type FormatsResponse = {
title: string | null;
duration: number | null;
extractor: string | null;
video_id: string | null;
max_size_bytes: number;
options: FormatOption[];
};
// Probe available formats for a URL (no auth required)
export async function probeFormats(url: string): Promise<FormatsResponse> {
const res = await Client.public.post("/api/downloader/formats/", { url });
return res.data as FormatsResponse;
}
// Download selected format as a Blob and resolve filename from headers
export async function downloadFormat(url: string, format_id: string): Promise<{ blob: Blob; filename: string }> {
const res = await Client.public.post(
"/api/downloader/download/",
{ url, format_id },
{ responseType: "blob" }
);
// Try to parse Content-Disposition filename first, then X-Filename (exposed by backend)
const cd = res.headers?.["content-disposition"] as string | undefined;
const xfn = res.headers?.["x-filename"] as string | undefined;
const filename =
parseContentDispositionFilename(cd) ||
(xfn && xfn.trim()) ||
inferFilenameFromUrl(url, (res.headers?.["content-type"] as string | undefined)) ||
"download.bin";
return { blob: res.data as Blob, filename };
}
// Deprecated types kept for compatibility if referenced elsewhere
export type Choices = { file_types: string[]; qualities: string[] };
export type DownloadJobResponse = {
id: string;
status: "pending" | "running" | "finished" | "failed";
detail?: string;
download_url?: string;
progress?: number; // 0-100
progress?: number;
};
// Fallback when choices endpoint is unavailable or models are hardcoded
const FALLBACK_CHOICES: Choices = {
file_types: ["auto", "video", "audio"],
qualities: ["best", "good", "worst"],
};
// Helpers
function parseContentDispositionFilename(cd?: string): string | null {
if (!cd) return null;
// filename*=UTF-8''encoded or filename="plain"
const utf8Match = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
const plainMatch = cd.match(/filename\s*=\s*"([^"]+)"/i) || cd.match(/filename\s*=\s*([^;]+)/i);
return plainMatch?.[1]?.trim() || null;
}
/**
* Fetch dropdown choices from backend (adjust path to match your Django views).
* Expected response shape:
* { file_types: string[], qualities: string[] }
*/
export async function getChoices(): Promise<Choices> {
function inferFilenameFromUrl(url: string, contentType?: string): string {
try {
const res = await Client.auth.get("/api/downloader/choices/");
return res.data as Choices;
const u = new URL(url);
const last = u.pathname.split("/").filter(Boolean).pop();
if (last) return last;
} catch {
return FALLBACK_CHOICES;
// ignore
}
if (contentType) {
const ext = contentTypeToExt(contentType);
return `download${ext ? `.${ext}` : ""}`;
}
return "download.bin";
}
/**
* Submit a new download job (adjust path/body to your viewset).
* Example payload: { url, file_type, quality }
*/
export async function submitDownload(payload: DownloadPayload): Promise<DownloadJobResponse> {
const res = await Client.auth.post("/api/downloader/jobs/", payload);
return res.data as DownloadJobResponse;
}
/**
* Get job status by ID. Returns progress, status, and download_url when finished.
*/
export async function getJobStatus(id: string): Promise<DownloadJobResponse> {
const res = await Client.auth.get(`/api/downloader/jobs/${id}/`);
return res.data as DownloadJobResponse;
function contentTypeToExt(ct: string): string | null {
const map: Record<string, string> = {
"video/mp4": "mp4",
"audio/mpeg": "mp3",
"audio/mp4": "m4a",
"audio/aac": "aac",
"audio/ogg": "ogg",
"video/webm": "webm",
"audio/webm": "webm",
"application/octet-stream": "bin",
};
return map[ct] || null;
}