Files
ytplayer/plans/patches/016-client-intake.diff

92 lines
4.1 KiB
Diff

--- a/frontend/p2p-client.js
+++ b/frontend/p2p-client.js
@@ -6,6 +6,9 @@
* changed() a save/delete happened → re-report soon
* device() { deviceId, secret } or null
* authHeaders() { 'X-Device': … } for other P2P calls
+ * contribute(videoId) / unknownVideos()
+ * hand a saved file the server doesn't
+ * know yet to /api/p2p/intake (plan 016)
* onMessage(type, fn) / signal(to, data) / peer()
* live /ws/p2p socket (plan 014): the
* device is "online" while it is open
@@ -33,6 +36,7 @@
let again = false;
let timer = null;
let lastSync = 0;
+ let lastUnknown = []; // cids the server did not recognise at the last report
function device() {
try {
@@ -130,6 +134,7 @@
if (r.status === 401) { try { localStorage.removeItem(KEY); } catch { /* ignore */ } return null; }
const j = await r.json();
if (!j || !j.ok) return null;
+ lastUnknown = Array.isArray(j.unknown) ? j.unknown : [];
const accepted = new Set(j.accepted || []);
for (const rec of recs) {
if (rec.cid && accepted.has(rec.cid) && rec.state !== 'verified') { rec.state = 'verified'; await window.DeviceDB.putFile(rec); }
@@ -151,6 +156,33 @@
return j;
}
+ // ---- intake (plan 016) ---------------------------------------------------------
+ // Send one saved video to the server so it can hash + validate it and add its
+ // cid to the catalog. Uploads the whole file: only on the user's request
+ // ("Verify & share") or when the server asks for a copy (plan 018).
+ async function contribute(videoId, { cid = null, title = '', channel = '' } = {}) {
+ const dev = await ensureDevice();
+ const rec = await window.DeviceDB.getFile(videoId);
+ const file = window.OPFS.getFileObject ? await window.OPFS.getFileObject(videoId) : null;
+ if (!file) return { ok: false, error: 'not saved on this device' };
+ const h = { 'X-Device': dev.deviceId + '.' + dev.secret };
+ const t = await (await fetch('/api/p2p/intake', {
+ method: 'POST', headers: { ...h, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ videoId, cid: cid || (rec && rec.cid) || undefined, size: file.size, title, channel }),
+ })).json().catch(() => ({ ok: false, error: 'server unreachable' }));
+ if (!t.ok || t.known) { if (t.known) changed(); return t; }
+ const r = await (await fetch(t.url, { method: 'PUT', headers: h, body: file }))
+ .json().catch(() => ({ ok: false, error: 'upload failed' }));
+ if (r.ok) changed();
+ return r;
+ }
+
+ async function unknownVideos() {
+ if (!lastUnknown.length || !window.DeviceDB) return [];
+ const recs = await window.DeviceDB.listFiles();
+ return recs.filter((r) => r.cid && lastUnknown.includes(r.cid)).map((r) => r.videoId);
+ }
+
// ---- presence socket (/ws/p2p) — plan 014 ------------------------------------
// Open while sharing OR receiving is on. Credentials go in the first message,
// never the URL. Reconnects with backoff (5 s … 5 min).
@@ -258,7 +290,7 @@
}
window.P2PClient = {
- start, changed, sync, device, authHeaders, config,
+ start, changed, sync, device, authHeaders, config, contribute, unknownVideos,
onMessage, signal, peer: () => myPeer, isConnected: () => !!(ws && ws.readyState === 1 && myPeer),
};
}());
--- a/frontend/opfs.js
+++ b/frontend/opfs.js
@@ -108,6 +108,17 @@
}
},
+ // The saved File itself (disk-backed, not read into memory) — used as an
+ // upload body by P2P intake. null when not saved.
+ async getFileObject(videoId) {
+ try {
+ const found = await findHandle(videoId);
+ return found ? await found[0].getFile() : null;
+ } catch {
+ return null;
+ }
+ },
+
// Bytes [offset, offset+length) of a saved video — answers the server's
// P2P range challenges (p2p-client.js). null when the file is missing.
async readRange(videoId, offset, length) {