feat(webseed): upload form accepts multiple web seeds (slice 2/3)

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-19 08:36:13 -04:00
1 parent cf98d06cca
commit ce7ef9ef2d
4 files changed
+44 -13

No files matched your search

+3 -2
View File
@@ -376,8 +376,9 @@
<!-- Additional Technical Sections (Web Seeds, etc) -->
<div class="grid gap-3 md:grid-cols-2">
<div class="space-y-2">
<label class="block text-xs font-medium text-muted-strong">Web Seed (ws)</label>
<input id="input-ws" type="url" class="input text-sm" placeholder="https://..." />
<label class="block text-xs font-medium text-muted-strong">Web seeds (one per line)</label>
<textarea id="input-ws" rows="2" class="textarea text-sm" placeholder="https://cdn.example.com/video.mp4&#10;https://backup.example.com/video.mp4"></textarea>
<p class="text-xs text-muted">Add a backup origin so the swarm can keep streaming if the primary is overloaded. Each must serve the exact same file.</p>
</div>
<div class="space-y-2">
<label class="block text-xs font-medium text-muted-strong">Torrent File (xs)</label>
+15 -10
View File
@@ -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;
}
+7 -1
View File
@@ -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
+19
View File
@@ -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",