Share target: shared YouTube links play, shared audio/video uploads to the user's library; page shortcuts
- manifest share_target (POST multipart: title/text/url + audio/video files) and eight shortcuts (Search, Queue, Saved, Downloads, History, Library, Notes, Settings). /?view=<page> now actually opens that page — the old shortcuts pointed at it but nothing read it. - sw.js receives the share: files are parked in the ytp-share-inbox cache (not a versioned shell cache, so deploys never evict it) and the app is opened; links/text go to /?shared=. A server POST /share-target fallback covers the first visit before a worker is in control (links only). - The app plays a YouTube link found anywhere in the shared text (watch, youtu.be, shorts, live, embed, music; keeps t=), otherwise searches the text. /?shared=<link> also works from an iOS Shortcut (iOS has no share target). - Shared files upload with progress to PUT /api/uploads/shared and join a "Shared uploads" playlist; network failures stay in the inbox for the next open, refusals are dropped with the reason. - The public route is bounded: audio/video only (ffprobe-validated, error text without server paths), 500 MB per file, 2 GB and 20 a day per device, 50 GB for all shared uploads, one at a time per device (all env-tunable). Shared uploads are unlisted: reachable by id, never in anyone else's search. uploads gains owner + listed columns (idempotent ALTER).
This commit is contained in:
33
server/db.js
33
server/db.js
@@ -212,6 +212,11 @@ export async function initDb() {
|
||||
last_used_at INTEGER
|
||||
);
|
||||
`);
|
||||
// Uploads shared from devices (PWA share target): who sent it, and whether it
|
||||
// is listed in everyone's search (admin uploads) or reachable only by link.
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Shared video notes (lyrics / chapters) ---------------------------------
|
||||
@@ -725,16 +730,30 @@ 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,
|
||||
owner: r.owner || null, listed: r.listed === undefined || r.listed === null ? true : Number(r.listed) === 1,
|
||||
});
|
||||
|
||||
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],
|
||||
sql: `INSERT INTO uploads (id, kind, title, artist, album, duration, ext, mime, size, art, owner, listed, 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,
|
||||
u.owner || null, u.listed === false ? 0 : 1],
|
||||
});
|
||||
}
|
||||
|
||||
// Quota bookkeeping for device-shared uploads.
|
||||
export async function sharedUploadUsage(owner) {
|
||||
const r = await db.execute({
|
||||
sql: `SELECT ifnull(sum(size), 0) AS bytes,
|
||||
ifnull(sum(CASE WHEN created_at > unixepoch() - 86400 THEN 1 ELSE 0 END), 0) AS today
|
||||
FROM uploads WHERE owner = ?`,
|
||||
args: [owner],
|
||||
});
|
||||
const t = await db.execute('SELECT ifnull(sum(size), 0) AS bytes FROM uploads WHERE owner IS NOT NULL');
|
||||
return { bytes: Number(r.rows[0].bytes) || 0, today: Number(r.rows[0].today) || 0, totalShared: Number(t.rows[0].bytes) || 0 };
|
||||
}
|
||||
|
||||
export async function setUploadArt(id, art) {
|
||||
await db.execute({ sql: 'UPDATE uploads SET art = ? WHERE id = ?', args: [art, id] });
|
||||
}
|
||||
@@ -745,16 +764,18 @@ export async function getUpload(id) {
|
||||
}
|
||||
|
||||
// Newest first; `q` matches title/artist/album (case-insensitive).
|
||||
export async function listUploads({ q = '', limit = 100 } = {}) {
|
||||
// `listedOnly`: public search shows admin uploads, never device-shared ones.
|
||||
export async function listUploads({ q = '', limit = 100, listedOnly = false } = {}) {
|
||||
const like = `%${String(q).toLowerCase()}%`;
|
||||
const vis = listedOnly ? 'ifnull(listed, 1) = 1 AND ' : '';
|
||||
const r = q
|
||||
? await db.execute({
|
||||
sql: `SELECT * FROM uploads
|
||||
WHERE lower(title) LIKE ? OR lower(ifnull(artist, '')) LIKE ? OR lower(ifnull(album, '')) LIKE ?
|
||||
WHERE ${vis}(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] });
|
||||
: await db.execute({ sql: `SELECT * FROM uploads ${listedOnly ? 'WHERE ifnull(listed, 1) = 1 ' : ''}ORDER BY created_at DESC LIMIT ?`, args: [limit] });
|
||||
return r.rows.map(uploadRow);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user