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:
@@ -160,10 +160,42 @@ self.addEventListener('activate', (e) => {
|
||||
});
|
||||
|
||||
// ---- Fetch: routing logic ----
|
||||
// ---- Share target (manifest share_target) ----
|
||||
// The OS share sheet POSTs the shared link/text/files here. Files are parked in
|
||||
// a cache the app reads on open (name does not start with "ytplayer-", so no
|
||||
// deploy ever evicts it), then the page is opened to process them.
|
||||
const SHARE_INBOX = 'ytp-share-inbox';
|
||||
async function receiveShare(request) {
|
||||
let form;
|
||||
try { form = await request.formData(); } catch { return Response.redirect('/?shared=', 303); }
|
||||
const files = form.getAll('media').filter((f) => f && typeof f !== 'string' && f.size);
|
||||
if (files.length) {
|
||||
const inbox = await caches.open(SHARE_INBOX);
|
||||
let n = 0;
|
||||
for (const f of files) {
|
||||
const key = `/__share/${Date.now()}-${n++}`;
|
||||
await inbox.put(key, new Response(f, { headers: {
|
||||
'Content-Type': f.type || 'application/octet-stream',
|
||||
'X-Name': encodeURIComponent(f.name || 'shared'),
|
||||
'Content-Length': String(f.size),
|
||||
} }));
|
||||
}
|
||||
return Response.redirect('/?share=files', 303);
|
||||
}
|
||||
const pick = (k) => String(form.get(k) || '').trim();
|
||||
const text = [pick('url'), pick('text'), pick('title')].filter(Boolean).join(' ');
|
||||
return Response.redirect('/?shared=' + encodeURIComponent(text.slice(0, 2000)), 303);
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', (e) => {
|
||||
const { request } = e;
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method === 'POST' && url.pathname === '/share-target') {
|
||||
e.respondWith(receiveShare(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Only intercept GET/HEAD — let POST (sync endpoint) go through unmodified
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user