9.2 KiB
9.2 KiB
id, title, created, depends_on, est_files
| id | title | created | depends_on | est_files | |
|---|---|---|---|---|---|
| 017-peer-transfer-1faaa7 | Download a verified file from another device over WebRTC | 2026-09-29 |
|
6 |
017 — Peer download over WebRTC
Objective
Implements flow 6 of docs/p2p-architecture.md. After this plan:
window.P2PTransferserves this device's saved files to other devices (only whilesettings.p2pShare !== false, one upload at a time) and downloads from them over a WebRTC data channel, signalled through/ws/p2p(plan 014). STUN only.- The receiver writes through
p2p-recv-worker.jsintovideos/<id>.p2p.partwhile hashing, and renames to<id>.mp4ONLY when the SHA-256 equals the cid. A wrong, corrupted or short file leaves nothing behind. - When a video fails to load (YouTube and the server both failing), and
settings.p2pReceive !== false, the player shows 📡 Get it from a device (N online) next to ↻ Retry. On success the video is saved offline, registered asverified(so this device becomes a holder at its next report) and plays.
Pre-tested in Chromium with two browser contexts and a real hub
(plans/harness/transfer-*.js): 5 MiB file delivered and verified in ~2.5 s; unknown cid →
peer declined: not here; holder with a corrupted same-size file → content hash mismatch
and no file kept.
Context the executor must NOT rediscover
plans/patches/017-p2p-transfer-new.diffcreatesfrontend/p2p-transfer.jsandfrontend/p2p-recv-worker.js.- Script order in
frontend/index.htmlafter plan 015:… sha256.js, device-db.js, p2p-client.js, p2p-core.js, async-guard.js, sw-update.js, app.js. frontend/sw.jsSHELLhas'/p2p-core.js',(plan 015).frontend/app.js:Player.loadVideocatch block (~line 1735):} catch (err) { showSpinner(false); toast('⚠ ' + err.message); // Show retry button in the player pane const retry = els.playerPane.querySelector('.retry-btn'); if (retry) retry.remove(); const btn = document.createElement('button'); btn.className = 'retry-btn'; btn.textContent = '↻ Retry'; btn.addEventListener('click', () => { btn.remove(); Player.loadVideo(videoObj, { preferStream, resume, reveal, nocache }); }); els.playerPane.appendChild(btn); }- globals/helpers:
downloading(Set, ~line 372),cachedIds,cacheMutations,markCardCacheState(id, state)(~1351),updateDownloadBadge()(~1313),renderSidebar(),recordDeviceFile(id, res)(plan 012),toast(). boot()has (plan 013):if (WEB && window.P2PClient) { window.P2PClient.start({ getSettings: () => data.settings, getProfile: () => (data.profile && data.profile.name) || '' }); }
window.P2PCore.pickPeers(payload, cid)→[{ peer, cid, size }]online holders (plan 015).
Steps
- From the repo root:
git apply plans/patches/017-p2p-transfer-new.diff(STOP on failure). frontend/index.html— add<script src="p2p-transfer.js"></script>directly after thep2p-core.jsline.frontend/sw.jsSHELL— add'/p2p-transfer.js',and'/p2p-recv-worker.js',directly after'/p2p-core.js',.frontend/styles.css— append at the end of the file:/* P2P: "Get it from a device" next to Retry (plan 017) */ .peer-btn { margin-left: 8px; }frontend/app.jsboot()— inside theif (WEB && window.P2PClient) { … }block, after thestart(...)call add:if (window.P2PTransfer) window.P2PTransfer.start({ canShare: () => data.settings.p2pShare !== false });frontend/app.js— directly ABOVEfunction updateNowPlayingActions() {(next to plan 015'srenderAvailability) add:// Fetch a verified copy from another device (docs/p2p-architecture.md flow 6). // Download-then-play: on success the file is saved offline like any save. async function getFromPeers(videoObj, c, peers, onProgress) { const id = videoObj.id; downloading.add(id); markCardCacheState(id, 'downloading'); updateDownloadBadge(); try { const res = await window.P2PTransfer.download({ videoId: id, cid: c.cid, size: c.size, peers, onProgress }); if (res.ok) { cachedIds.add(id); cacheMutations++; recordDeviceFile(id, { sha256: res.sha256, expectedSha: c.cid, size: res.size }); toast(`Saved “${videoObj.title || id}” from another device ✓`); } return res; } finally { downloading.delete(id); markCardCacheState(id, cachedIds.has(id) ? 'cached' : 'none'); updateDownloadBadge(); renderSidebar(); } } // Offered when a video won't load: only if some device holding it is online now. async function offerPeerDownload(videoObj) { if (!WEB || !window.P2PTransfer || !window.P2PCore || data.settings.p2pReceive === false) return; const id = videoObj && videoObj.id; if (!id || videoObj.custom || cachedIds.has(id)) return; let payload = null; try { const r = await fetch(`/api/p2p/holders?v=${encodeURIComponent(id)}`); payload = r.ok ? await r.json() : null; } catch { return; } const c = payload && Array.isArray(payload.cids) ? payload.cids[0] : null; const peers = c ? window.P2PCore.pickPeers(payload, c.cid) : []; if (!peers.length || !els.playerPane.querySelector('.retry-btn')) return; // nobody online / user moved on const old = els.playerPane.querySelector('.peer-btn'); if (old) old.remove(); const btn = document.createElement('button'); btn.className = 'retry-btn peer-btn'; btn.textContent = `📡 Get it from a device (${peers.length} online)`; btn.addEventListener('click', async () => { btn.disabled = true; const res = await getFromPeers(videoObj, c, peers, (got, total) => { btn.textContent = `📡 ${Math.min(99, Math.round((got / total) * 100))}%…`; }); if (res.ok) { els.playerPane.querySelectorAll('.retry-btn').forEach((b) => b.remove()); Player.loadVideo(videoObj); } else { btn.disabled = false; btn.textContent = '📡 Try again'; toast('⚠ ' + res.error); } }); els.playerPane.appendChild(btn); }frontend/app.jsPlayer.loadVideocatch block — directly afterels.playerPane.appendChild(btn);addofferPeerDownload(videoObj);. Also, in the Retry button's click handler, changebtn.remove();toels.playerPane.querySelectorAll('.retry-btn').forEach((b) => b.remove());so the peer button goes away with it.
Out of scope / do NOT touch
- No TURN server, no progressive (streaming) playback from peers, no partial seeding.
- Don't change
p2p-client.jsor the server.
Verification
cd /home/user/ytplayer && node --check frontend/app.js frontend/p2p-transfer.js frontend/p2p-recv-worker.js && echo FRONT_OK
node --test frontend/*.test.js 2>&1 | grep -E "^# (pass|fail)"
grep -c "p2p-transfer.js\|p2p-recv-worker.js" frontend/index.html frontend/sw.js
cd server && bun install >/dev/null 2>&1
bun ../plans/harness/transfer-server.js >/tmp/ytp017.log 2>&1 & SRV=$!; sleep 3
cd ../plans/harness && (npm ls playwright >/dev/null 2>&1 || npm i --no-save playwright >/dev/null 2>&1); timeout 120 node transfer-check.mjs 2>&1 | grep -v "status of 500"
kill $SRV; true
Expected: FRONT_OK; # fail 0; index.html 1, sw.js 2; browser JSON containing
"good":{"ok":true, "list":[["gotAAAAAAAA",5243657]], "bad":{"ok":false,"error":"peer declined: not here"},
"corrupt":{"r":{"ok":false,"error":"content hash mismatch"},"names":["gotAAAAAAAA.mp4"]}.
Report format (executor: follow exactly)
Output ONLY the following, no other prose:
git diff(unified) of all changes.- Raw output of the Verification commands.
Findings:— max 10 lines.
Do not commit. Do not push. Do not touch files outside the Steps.
Execution log
- Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0.
- Orchestrator re-ran Verification:
p2p-transfer.jsandp2p-recv-worker.jsbyte-identical to the pre-tested versions; all frontend files passnode --check; 61 frontend tests pass; index.html references the new script once and sw.js twice; two-browser WebRTC check reproduced the expected JSON (5 MiB delivered and hash-verified, unknown cidpeer declined: not here, corrupted holdercontent hash mismatchwith no partial file left); full app load in Chromium: no JS errors, all globals present includingP2PTransfer, share/receive on by default, device online. No leftover processes. - Orchestrator note: the executor's
npm/bun installleft an untracked rootbun.lock; deleted before commit (not part of the plan). - Executor Findings (verbatim): All syntax checks passed (FRONT_OK). All unit tests passed (61 pass, 0 fail). Script inclusions correct: index.html has 1 entry, sw.js has 2 entries. P2P transfer test executed successfully with expected JSON output containing good/bad/corrupt test cases. New functions getFromPeers and offerPeerDownload added correctly to app.js. P2P transfer and recv worker files created by git patch successfully.