fix(cloud): Range-streaming proxy for peer media so it plays/seeks (B3)

Peer media (music/video) wouldn't play: the frontend downloaded the whole
file via RPC as base64 and made a non-seekable Blob URL, so <video>/large
<audio> stalled and big files hit the RPC timeout.

Add GET /api/peer-content/<onion>/<id> — a same-origin, session-gated proxy
that forwards the browser's Range header to the peer's /content/<id> (which
already returns 206 Partial Content) and passes status + Content-Range +
Content-Type back. PeerFiles.playMedia() now points <video>/<audio> at this
streaming URL for free content instead of buffering a base64 blob, so the
player can seek and start immediately. Onion/id validated to prevent
SSRF/path traversal. (Paid preview keeps its existing flow.)

Verified: cargo build --release EXIT 0; vue-tsc --noEmit EXIT 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-15 13:46:51 -04:00
co-authored by Claude Opus 4.8
parent 4cac6bc835
commit 5c8707432b
4 changed files with 99 additions and 1 deletions
+16
View File
@@ -537,6 +537,22 @@ async function playMedia(item: CatalogItem) {
const paid = isPaidItem(item.access)
// Free content: stream via the Range-capable proxy (B3) so the player can
// seek and start instantly, instead of downloading the whole file as a
// base64 blob into a non-seekable Blob URL (which broke video/large audio).
if (!paid) {
const streamUrl = `/api/peer-content/${encodeURIComponent(onion)}/${encodeURIComponent(item.id)}`
if (item.mime_type.startsWith('audio/')) {
audioPlayer.play(streamUrl, item.filename.split('/').pop() || item.filename)
} else if (item.mime_type.startsWith('video/')) {
videoPlayerItem.value = item
videoPlayerUrl.value = streamUrl
videoPlayerPaid.value = false
}
return
}
// Paid content: use the preview/download flow below.
// If we already have a preview blob URL, use it
const existingUrl = previewUrls[item.id]
if (existingUrl) {