import fs from "node:fs"; import type { IncomingMessage, ServerResponse } from "node:http"; import path from "node:path"; import type { Connect, Plugin, ViteDevServer } from "vite"; import sqlite3 from "better-sqlite3"; import { Tokenizer } from "./tokenizer"; export function mp3DownloaderPlugin(): Plugin { return { name: "mp3-downloader", async configureServer(server: ViteDevServer) { const db = sqlite3(".sentence-mp3.db"); db.exec(/* sql */ ` CREATE TABLE IF NOT EXISTS sentence ( id INTEGER NOT NULL PRIMARY KEY, ko TEXT NOT NULL, original TEXT NOT NULL, [url] TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_sentence_u_url ON sentence([url]); CREATE VIRTUAL TABLE IF NOT EXISTS sentence_fts USING fts5 ( ko, original UNINDEXED, [url] UNINDEXED, content='sentence', content_rowid='id' ); -- Triggers to keep the FTS index up to date. CREATE TRIGGER IF NOT EXISTS t1_ai AFTER INSERT ON sentence BEGIN INSERT INTO sentence_fts(rowid, ko) VALUES (new.id, new.ko); END; CREATE TRIGGER IF NOT EXISTS t1_ad AFTER DELETE ON sentence BEGIN INSERT INTO sentence_fts(fts_idx, rowid, ko) VALUES('delete', old.id, old.ko); END; CREATE TRIGGER IF NOT EXISTS t1_au AFTER UPDATE ON sentence BEGIN INSERT INTO sentence_fts(fts_idx, rowid, ko) VALUES('delete', old.id, old.ko); INSERT INTO sentence_fts(rowid, ko) VALUES (new.id, new.ko); END; `); // Vite dev server middlewares are connect-style: (req, res, next) server.middlewares.use(async (req, res, next) => { const reqUrl = req.url; if (!reqUrl || !reqUrl.startsWith("/uploads/")) return next(); const url = new URL(reqUrl, "http://localhost"); if (!url.pathname.endsWith(".mp3")) return next(); try { await mp3Downloader(db, await Tokenizer.init(), url, res, next); } catch (e) { console.error(e, reqUrl); next(); } }); }, }; } async function mp3Downloader( db: ReturnType, tokenizer: Tokenizer, url: URL, res: ServerResponse, next: Connect.NextFunction, ) { const downloadURL = new URL( `https://www.howtostudykorean.com/wp-content${url.pathname}`, ); const text = url.searchParams.get("text"); if (text) { db.prepare( "INSERT OR IGNORE INTO sentence ([url], ko, original) VALUES (?, ?, ?)", ).run(downloadURL.href, tokenizer.normalize(text), text); } const segs = url.pathname.slice(1).split("/"); const filename = segs.pop()!; const saveroot = "docs/public"; const saveto = path.join(saveroot, ...segs, filename); if (fs.existsSync(saveto)) { return next(); } fs.mkdirSync(path.join(saveroot, ...segs), { recursive: true }); const r = await fetch(downloadURL, { redirect: "follow" }); if (!r.ok) { console.error(r); return next(); } fs.writeFileSync(saveto, Buffer.from(await r.arrayBuffer())); // console.log(fs.statSync(saveto)); console.log("saved to", saveto); res.setHeader("Content-Type", "audio/mpeg"); fs.createReadStream(saveto).pipe(res); }