feat: YouTube music download system with admin dashboard
Sidecar architecture: edge functions (auth + DB) → youtube-worker container (youtubei.js for playlist metadata, yt-dlp for audio downloads). - Edge functions: youtube-playlist, youtube-process, youtube-status (CRUD) - youtube-worker sidecar: Node.js + yt-dlp, downloads M4A to Supabase Storage - Admin music page: import playlists, process queue, inline genre editing - 12 music genres with per-track assignment and import-time defaults - DB migrations: download_jobs, download_items tables with genre column - Deploy script and CI workflow for edge functions + sidecar
This commit is contained in:
95
supabase/functions/main/index.ts
Normal file
95
supabase/functions/main/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as jose from "https://deno.land/x/jose@v4.14.4/index.ts";
|
||||
|
||||
console.log("main function started");
|
||||
|
||||
const JWT_SECRET = Deno.env.get("JWT_SECRET");
|
||||
const VERIFY_JWT = Deno.env.get("VERIFY_JWT") === "true";
|
||||
|
||||
function getAuthToken(req: Request) {
|
||||
const authHeader = req.headers.get("authorization");
|
||||
if (!authHeader) {
|
||||
throw new Error("Missing authorization header");
|
||||
}
|
||||
const [bearer, token] = authHeader.split(" ");
|
||||
if (bearer !== "Bearer") {
|
||||
throw new Error(`Auth header is not 'Bearer {token}'`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function isValidJWT(jwt: string): Promise<boolean> {
|
||||
if (!JWT_SECRET) {
|
||||
console.error("JWT_SECRET not available for token verification");
|
||||
return false;
|
||||
}
|
||||
const encoder = new TextEncoder();
|
||||
const secretKey = encoder.encode(JWT_SECRET);
|
||||
try {
|
||||
await jose.jwtVerify(jwt, secretKey);
|
||||
} catch (e) {
|
||||
console.error("JWT verification error", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
if (req.method !== "OPTIONS" && VERIFY_JWT) {
|
||||
try {
|
||||
const token = getAuthToken(req);
|
||||
const valid = await isValidJWT(token);
|
||||
if (!valid) {
|
||||
return new Response(JSON.stringify({ msg: "Invalid JWT" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return new Response(JSON.stringify({ msg: e.toString() }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
const { pathname } = url;
|
||||
const path_parts = pathname.split("/");
|
||||
const service_name = path_parts[1];
|
||||
|
||||
if (!service_name || service_name === "") {
|
||||
return new Response(
|
||||
JSON.stringify({ msg: "missing function name in request" }),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const servicePath = `/home/deno/functions/${service_name}`;
|
||||
console.error(`serving the request with ${servicePath}`);
|
||||
|
||||
const memoryLimitMb = 512;
|
||||
const workerTimeoutMs = 10 * 60 * 1000;
|
||||
const noModuleCache = false;
|
||||
const importMapPath = null;
|
||||
const envVarsObj = Deno.env.toObject();
|
||||
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]);
|
||||
|
||||
try {
|
||||
const worker = await EdgeRuntime.userWorkers.create({
|
||||
servicePath,
|
||||
memoryLimitMb,
|
||||
workerTimeoutMs,
|
||||
noModuleCache,
|
||||
importMapPath,
|
||||
envVars,
|
||||
});
|
||||
return await worker.fetch(req);
|
||||
} catch (e) {
|
||||
const error = { msg: e.toString() };
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user