Fetch lyrics from LRCLIB (synced when available) and serve an admin media library of uploaded video and audio with cover art and embedded lyrics
This commit is contained in:
61
server/db.js
61
server/db.js
@@ -142,6 +142,23 @@ export async function initDb() {
|
||||
CREATE INDEX IF NOT EXISTS idx_note_revs_time
|
||||
ON video_note_revs (created_at DESC);
|
||||
|
||||
-- Admin uploads: the server's own video/audio library, searched next to
|
||||
-- YouTube. Files live in UPLOAD_DIR as <id>.<ext> (+ <id>.art.jpg).
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id TEXT PRIMARY KEY, -- upl_<hex>
|
||||
kind TEXT NOT NULL, -- video | audio
|
||||
title TEXT NOT NULL,
|
||||
artist TEXT,
|
||||
album TEXT,
|
||||
duration REAL NOT NULL DEFAULT 0,
|
||||
ext TEXT NOT NULL,
|
||||
mime TEXT NOT NULL,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
art TEXT, -- 'embedded' | 'file' | NULL
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
plays INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- API tokens for scripts (lyrics injection etc.). Only a SHA-256 of the
|
||||
-- token is stored; the plaintext is shown once when it is created.
|
||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
@@ -561,3 +578,47 @@ export async function getUserData(fingerprint) {
|
||||
|
||||
return { lastVersion, playlists, history };
|
||||
}
|
||||
|
||||
// ---- Uploads (the server's own media library) ---------------------------------
|
||||
|
||||
const uploadRow = (r) => ({
|
||||
id: r.id, kind: r.kind, title: r.title, artist: r.artist || '', album: r.album || '',
|
||||
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,
|
||||
});
|
||||
|
||||
export async function createUpload(u) {
|
||||
await db.execute({
|
||||
sql: `INSERT INTO uploads (id, kind, title, artist, album, duration, ext, mime, size, art, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 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],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUpload(id) {
|
||||
const r = await db.execute({ sql: 'SELECT * FROM uploads WHERE id = ?', args: [id] });
|
||||
return r.rows[0] ? uploadRow(r.rows[0]) : null;
|
||||
}
|
||||
|
||||
// Newest first; `q` matches title/artist/album (case-insensitive).
|
||||
export async function listUploads({ q = '', limit = 100 } = {}) {
|
||||
const like = `%${String(q).toLowerCase()}%`;
|
||||
const r = q
|
||||
? await db.execute({
|
||||
sql: `SELECT * FROM uploads
|
||||
WHERE lower(title) LIKE ? OR lower(ifnull(artist, '')) LIKE ? OR lower(ifnull(album, '')) LIKE ?
|
||||
ORDER BY created_at DESC LIMIT ?`,
|
||||
args: [like, like, like, limit],
|
||||
})
|
||||
: await db.execute({ sql: 'SELECT * FROM uploads ORDER BY created_at DESC LIMIT ?', args: [limit] });
|
||||
return r.rows.map(uploadRow);
|
||||
}
|
||||
|
||||
export async function deleteUpload(id) {
|
||||
const r = await db.execute({ sql: 'DELETE FROM uploads WHERE id = ?', args: [id] });
|
||||
return (r.rowsAffected || 0) > 0;
|
||||
}
|
||||
|
||||
export async function touchUpload(id) {
|
||||
db.execute({ sql: 'UPDATE uploads SET plays = plays + 1 WHERE id = ?', args: [id] }).catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user