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",