111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
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;
|
|
`);
|
|
|
|
let tokenizer: Tokenizer | null = null;
|
|
|
|
// 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 {
|
|
// if (!tokenizer) {
|
|
// tokenizer = await Tokenizer.init();
|
|
// }
|
|
|
|
await mp3Downloader(db, tokenizer, url, res, next);
|
|
} catch (e) {
|
|
console.error(e, reqUrl);
|
|
next();
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
async function mp3Downloader(
|
|
db: ReturnType<typeof sqlite3>,
|
|
tokenizer: Tokenizer,
|
|
url: URL,
|
|
res: ServerResponse<IncomingMessage>,
|
|
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);
|
|
}
|