From ce7ef9ef2d0813d790ec5ebf38f286a32bb9758a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:36:13 -0400 Subject: [PATCH] feat(webseed): upload form accepts multiple web seeds (slice 2/3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let uploaders declare a backup origin at creation time so the swarm can keep streaming when the primary CDN is overloaded. - upload-modal.html: the single "Web Seed (ws)" input becomes a "Web seeds (one per line)" textarea with help text explaining the backup-origin use case and the byte-identical-file requirement. - UploadModal: in upload mode the R2/CDN url stays the primary webseed and any textarea entries follow it (passed as [primaryUrl, textarea]); external mode already flowed the field through. All parsing stays in the payload layer. - videoNotePayload.normalizeWebSeedList now splits every entry (array OR string) on newlines/commas, so the upload-modal's [url, "a\nb"] shape and a plain multi-line string both expand + dedupe correctly. Mutation-verified (array entries no longer split fails the upload-shape test); build + full unit suite green. Lints relevant to the change (inline-styles, tokens, innerHTML) pass. Note: npm run lint also reports pre-existing file-size violations in unrelated files (SignerManager, watchHistory, playbackService, webtorrent, etc.) that grew in earlier commits — none are touched here. Flagging separately. Co-Authored-By: Claude Opus 4.8 --- components/upload-modal.html | 5 +++-- js/services/videoNotePayload.js | 25 +++++++++++++++---------- js/ui/components/UploadModal.js | 8 +++++++- tests/multi-webseed.test.mjs | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/components/upload-modal.html b/components/upload-modal.html index dfb1870f..a955c113 100644 --- a/components/upload-modal.html +++ b/components/upload-modal.html @@ -376,8 +376,9 @@
- - + + +

Add a backup origin so the swarm can keep streaming if the primary is overloaded. Each must serve the exact same file.

diff --git a/js/services/videoNotePayload.js b/js/services/videoNotePayload.js index 9c7a5551..bb6dc370 100644 --- a/js/services/videoNotePayload.js +++ b/js/services/videoNotePayload.js @@ -47,25 +47,30 @@ function normalizeString(value) { function normalizeWebSeedList(value) { const out = []; const seen = new Set(); + // Each entry may itself carry several URLs separated by newlines/commas (a + // textarea field), so split every entry regardless of whether the caller + // passed an array or a single string. const push = (entry) => { if (typeof entry !== "string") { return; } - const trimmed = entry.trim(); - if (!trimmed) { - return; + for (const piece of entry.split(/[\r\n,]+/)) { + const trimmed = piece.trim(); + if (!trimmed) { + continue; + } + const key = trimmed.toLowerCase(); + if (seen.has(key)) { + continue; + } + seen.add(key); + out.push(trimmed); } - const key = trimmed.toLowerCase(); - if (seen.has(key)) { - return; - } - seen.add(key); - out.push(trimmed); }; if (Array.isArray(value)) { value.forEach(push); } else if (typeof value === "string") { - value.split(/[\r\n,]+/).forEach(push); + push(value); } return out; } diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 3cd72700..ddd0399e 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -1073,7 +1073,13 @@ export class UploadModal { metadata.storagePointer = this.videoUploadState.storagePointer || deriveStoragePointerFromUrl(metadata.url, providerLabel); - metadata.ws = this.videoUploadState.url; // WebSeed is same as R2 URL + // The R2/CDN url is the primary webseed; any additional backup + // webseeds the uploader entered (one per line) follow it. + // normalizeVideoNotePayload splits + dedupes this list. + metadata.ws = [ + this.videoUploadState.url, + this.inputs.ws?.value || "", + ]; metadata.xs = this.torrentState.url || ""; // NIP-71 IMETA for the main video diff --git a/tests/multi-webseed.test.mjs b/tests/multi-webseed.test.mjs index 2f56721f..79001e05 100644 --- a/tests/multi-webseed.test.mjs +++ b/tests/multi-webseed.test.mjs @@ -81,6 +81,25 @@ test("a single webseed string still works (back-compat)", () => { assert.equal(payload.legacyFormData.ws, SEED_A); }); +test("upload-modal shape ([primaryUrl, textarea]) yields primary-first, all split", () => { + // This mirrors exactly what UploadModal hands over in upload mode: the CDN + // url as the primary entry, followed by the raw multi-line textarea value. + const cdn = "https://cdn.example.com/video.mp4"; + const { payload } = normalizeVideoNotePayload({ + title: "Upload shape", + url: cdn, + magnet: MAGNET, + ws: [cdn, `${SEED_A}\n${SEED_B}`], + }); + + const seeds = extractAllWebSeeds(payload.legacyFormData.magnet); + assert.ok(seeds.includes(cdn), "CDN url carried"); + assert.ok(seeds.includes(SEED_A), "first backup webseed carried"); + assert.ok(seeds.includes(SEED_B), "second backup webseed carried"); + // CDN stays the primary (first) seed. + assert.equal(payload.legacyFormData.ws, cdn); +}); + test("duplicate webseeds are not written twice", () => { const { payload } = normalizeVideoNotePayload({ title: "Dupe seed",