Add upload library management and ops diagnostics
This commit is contained in:
53
server/db.js
53
server/db.js
@@ -234,7 +234,9 @@ export async function initDb() {
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
art TEXT, -- 'embedded' | 'file' | NULL
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
plays INTEGER NOT NULL DEFAULT 0
|
||||
plays INTEGER NOT NULL DEFAULT 0,
|
||||
gen INTEGER NOT NULL DEFAULT 1,
|
||||
art_gen INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- "This lyric line is wrong" reports from service mode. One open row per
|
||||
@@ -294,6 +296,9 @@ export async function initDb() {
|
||||
for (const col of ['owner TEXT', 'listed INTEGER NOT NULL DEFAULT 1']) {
|
||||
try { await db.execute(`ALTER TABLE uploads ADD COLUMN ${col}`); } catch { /* already there */ }
|
||||
}
|
||||
try { await db.execute('ALTER TABLE uploads ADD COLUMN gen INTEGER NOT NULL DEFAULT 1'); } catch { /* already migrated */ }
|
||||
try { await db.execute('ALTER TABLE uploads ADD COLUMN art_gen INTEGER NOT NULL DEFAULT 1'); } catch { /* already migrated */ }
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS upload_owner_quotas (owner TEXT PRIMARY KEY,max_bytes INTEGER NOT NULL CHECK(max_bytes>=0),updated_at INTEGER NOT NULL)`);
|
||||
// Integrated loudness (EBU R128 LUFS) measured by devices in the browser and
|
||||
// shared, so each song is measured once for everyone.
|
||||
await db.execute(`CREATE TABLE IF NOT EXISTS loudness (
|
||||
@@ -956,12 +961,13 @@ const uploadRow = (r) => ({
|
||||
duration: Number(r.duration) || 0, ext: r.ext, mime: r.mime, size: Number(r.size) || 0,
|
||||
art: r.art || null, createdAt: Number(r.created_at), plays: Number(r.plays) || 0,
|
||||
owner: r.owner || null, listed: r.listed === undefined || r.listed === null ? true : Number(r.listed) === 1,
|
||||
gen: Number(r.gen) || 1, artGen: Number(r.art_gen) || 1,
|
||||
});
|
||||
|
||||
export async function createUpload(u) {
|
||||
await db.execute({
|
||||
sql: `INSERT INTO uploads (id, kind, title, artist, album, duration, ext, mime, size, art, owner, listed, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, unixepoch())`,
|
||||
sql: `INSERT INTO uploads (id, kind, title, artist, album, duration, ext, mime, size, art, owner, listed, gen, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, unixepoch())`,
|
||||
args: [u.id, u.kind, u.title, u.artist || null, u.album || null, u.duration || 0, u.ext, u.mime, u.size || 0, u.art || null,
|
||||
u.owner || null, u.listed === false ? 0 : 1],
|
||||
});
|
||||
@@ -980,7 +986,7 @@ export async function sharedUploadUsage(owner) {
|
||||
}
|
||||
|
||||
export async function setUploadArt(id, art) {
|
||||
await db.execute({ sql: 'UPDATE uploads SET art = ? WHERE id = ?', args: [art, id] });
|
||||
await db.execute({ sql: 'UPDATE uploads SET art = ?, art_gen = art_gen + 1 WHERE id = ?', args: [art, id] });
|
||||
}
|
||||
|
||||
export async function getUpload(id) {
|
||||
@@ -988,6 +994,45 @@ export async function getUpload(id) {
|
||||
return r.rows[0] ? uploadRow(r.rows[0]) : null;
|
||||
}
|
||||
|
||||
export async function updateUploadMeta(id, patch = {}) {
|
||||
const fields = ['title', 'artist', 'album', 'listed'].filter(k => patch[k] !== undefined);
|
||||
if (!fields.length) return !!(await getUpload(id));
|
||||
const values = fields.map(k => k === 'listed' ? (patch[k] ? 1 : 0) : String(patch[k] ?? '').slice(0, k === 'title' ? 300 : 200) || null);
|
||||
const result = await db.execute({ sql: `UPDATE uploads SET ${fields.map(k => `${k}=?`).join(',')} WHERE id=?`, args: [...values, id] });
|
||||
return !!result.rowsAffected;
|
||||
}
|
||||
|
||||
export async function replaceUploadFile(id, upload) {
|
||||
const result = await db.execute({ sql: `UPDATE uploads SET kind=?,title=?,artist=?,album=?,duration=?,ext=?,mime=?,size=?,art=?,gen=gen+1 WHERE id=?`,
|
||||
args: [upload.kind, upload.title, upload.artist || null, upload.album || null, upload.duration || 0, upload.ext, upload.mime, upload.size || 0, upload.art || null, id] });
|
||||
return !!result.rowsAffected;
|
||||
}
|
||||
|
||||
export async function listAdminUploads({ q = '', limit = 100, offset = 0 } = {}) {
|
||||
const args = [], where = q ? "WHERE lower(title || ' ' || ifnull(artist,'') || ' ' || ifnull(album,'')) LIKE ?" : '';
|
||||
if (q) args.push(`%${String(q).toLowerCase().replace(/[\\%_]/g, '\\$&')}%`);
|
||||
const rows = await db.execute({ sql: `SELECT * FROM uploads ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, args: [...args, Math.max(1, Math.min(200, limit)), Math.max(0, offset)] });
|
||||
const count = await db.execute({ sql: `SELECT COUNT(*) AS n FROM uploads ${where}`, args });
|
||||
const owners = await db.execute("SELECT ifnull(owner,'(admin)') AS owner,COUNT(*) AS count,COALESCE(SUM(size),0) AS bytes FROM uploads GROUP BY owner ORDER BY bytes DESC");
|
||||
return { uploads: rows.rows.map(uploadRow), total: Number(count.rows[0]?.n) || 0, owners: owners.rows.map(r => ({ owner: r.owner, count: Number(r.count), bytes: Number(r.bytes) })) };
|
||||
}
|
||||
export async function listUploadFileNames() {
|
||||
const r = await db.execute('SELECT id,ext FROM uploads');
|
||||
return r.rows.flatMap(row => [`${row.id}.${row.ext}`, `${row.id}.art.jpg`]);
|
||||
}
|
||||
export async function listUploadQuotas() {
|
||||
const r = await db.execute('SELECT owner,max_bytes,updated_at FROM upload_owner_quotas ORDER BY owner');
|
||||
return r.rows.map(x => ({ owner: x.owner, maxBytes: Number(x.max_bytes), updatedAt: Number(x.updated_at) }));
|
||||
}
|
||||
export async function getUploadQuota(owner) {
|
||||
const r = await db.execute({ sql: 'SELECT max_bytes FROM upload_owner_quotas WHERE owner=?', args: [owner] });
|
||||
return r.rows[0] ? Number(r.rows[0].max_bytes) : null;
|
||||
}
|
||||
export async function setUploadQuota(owner, maxBytes) {
|
||||
if (maxBytes == null || Number(maxBytes) <= 0) return db.execute({ sql: 'DELETE FROM upload_owner_quotas WHERE owner=?', args: [owner] });
|
||||
return db.execute({ sql: 'INSERT INTO upload_owner_quotas(owner,max_bytes,updated_at) VALUES(?,?,unixepoch()) ON CONFLICT(owner) DO UPDATE SET max_bytes=excluded.max_bytes,updated_at=excluded.updated_at', args: [owner, Math.floor(Number(maxBytes))] });
|
||||
}
|
||||
|
||||
// Newest first; `q` matches title/artist/album (case-insensitive).
|
||||
// `listedOnly`: public search shows admin uploads, never device-shared ones.
|
||||
export async function listUploads({ q = '', limit = 100, listedOnly = false } = {}) {
|
||||
|
||||
Reference in New Issue
Block a user