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:
Jonathan Sykes
2026-10-03 01:33:51 +08:00
parent 6581509ddd
commit 91627dc0a9
6 changed files with 405 additions and 12 deletions

View File

@@ -468,7 +468,7 @@ function fetchYoutube(q) {
const searchLibrary = async (q) => {
// This server's own library first — it still answers when YouTube is unreachable.
try { return (await notesDb.listUploads({ q, limit: 20 })).map(uploads.card); } catch { return []; }
try { return (await notesDb.listUploads({ q, limit: 20, listedOnly: true })).map(uploads.card); } catch { return []; }
};
// GET /api/search/local?q=<query> — videos this server already knows (from every
@@ -1553,6 +1553,16 @@ function cachedDownloadResponse(c, videoId, fp, row) {
});
}
// POST /share-target — the manifest share target when no service worker is in
// control yet (first visit): links and text still work; files need the worker.
app.post('/share-target', async (c) => {
let body = {};
try { body = await c.req.parseBody(); } catch { /* not a form */ }
const pick = (k) => (typeof body[k] === 'string' ? body[k].trim() : '');
const text = [pick('url'), pick('text'), pick('title')].filter(Boolean).join(' ').slice(0, 2000);
return c.redirect('/?shared=' + encodeURIComponent(text) + (body.media ? '&sharefail=1' : ''), 303);
});
// GET /api/download/:videoId/prepare[?hevc=1] — never blocks. Starts (or
// joins) the server-side fetch and reports where it is:
// ready → { gen, size, sha256 }: fetch /api/download/:id in ranges