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