mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(webseed): carry multiple web seeds through publish (slice 1/3)
Foundation for declaring more than one web seed per video so the WebTorrent swarm can pull pieces from an independent backup origin when the primary CDN is overloaded — multiple `ws=` params are the standard BEP19 carrier and the playback path already attaches every webseed it finds in the magnet. - magnetShared: add extractAllWebSeeds(), which returns every `ws=` URL (deduped, in order) instead of only the first like extractMagnetHints — so a magnet with several webseeds can be round-tripped without dropping the extras. - videoNotePayload: normalizeWebSeedList() accepts the ws field as an array OR a newline/comma-delimited string and feeds the full, deduped set into the magnet (plus the hosted CDN url, as before). A single ws string still works unchanged; the content `ws` field still reflects the primary seed. Upload/Edit UI to author and manage the list lands in slices 2 and 3. Mutation-verified (array-drop, split-drop, first-only, dedupe-loss all fail the new tests); build + full unit suite green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
512b6c3f6a
commit
cf98d06cca
3 files changed
+170
-5
No files matched your search
@@ -421,3 +421,38 @@ export function extractMagnetHints(rawValue) {
|
||||
}
|
||||
return hints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract every web seed (`ws=`) URL from a magnet, in order, deduped
|
||||
* case-insensitively. Unlike {@link extractMagnetHints} (which returns only the
|
||||
* first `ws`), this surfaces the full set so callers can round-trip a magnet
|
||||
* that carries multiple independent webseeds without silently dropping the
|
||||
* extras.
|
||||
*
|
||||
* @param {string} rawValue
|
||||
* @returns {string[]} Deduped web seed URLs in magnet order.
|
||||
*/
|
||||
export function extractAllWebSeeds(rawValue) {
|
||||
const { params } = normalizeMagnetInput(rawValue);
|
||||
const seeds = [];
|
||||
const seen = new Set();
|
||||
for (const param of params) {
|
||||
if (param.lowerKey !== "ws") {
|
||||
continue;
|
||||
}
|
||||
const value =
|
||||
typeof (param.decoded || param.value) === "string"
|
||||
? (param.decoded || param.value).trim()
|
||||
: "";
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
const key = value.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
seeds.push(value);
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
@@ -34,6 +34,42 @@ function normalizeString(value) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a web seed field into an ordered, deduped list of trimmed URLs.
|
||||
* Accepts an array (multi-webseed forms) or a string that may carry several
|
||||
* URLs separated by newlines/commas (a single textarea field). Preserving the
|
||||
* full list lets a video declare multiple independent webseeds (e.g. a CDN
|
||||
* plus a backup origin) instead of collapsing to a single `ws`.
|
||||
*
|
||||
* @param {string|string[]} value
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function normalizeWebSeedList(value) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const push = (entry) => {
|
||||
if (typeof entry !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeSha256Hex(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
@@ -342,7 +378,7 @@ export function normalizeVideoNotePayload(input) {
|
||||
const magnet = normalizeString(legacyPayload?.magnet || "");
|
||||
const thumbnail = normalizeString(legacyPayload?.thumbnail || "");
|
||||
const description = normalizeString(legacyPayload?.description || "");
|
||||
const ws = normalizeString(legacyPayload?.ws || "");
|
||||
const wsValues = normalizeWebSeedList(legacyPayload?.ws);
|
||||
const xs = normalizeString(legacyPayload?.xs || "");
|
||||
const storagePointer = normalizeStoragePointer(
|
||||
legacyPayload?.storagePointer || legacyPayload?.storage || ""
|
||||
@@ -415,10 +451,7 @@ export function normalizeVideoNotePayload(input) {
|
||||
}
|
||||
|
||||
if (legacyFormData.magnet) {
|
||||
const wsList = [];
|
||||
if (ws) {
|
||||
wsList.push(ws);
|
||||
}
|
||||
const wsList = [...wsValues];
|
||||
if (legacyFormData.url && /^https?:\/\//i.test(legacyFormData.url)) {
|
||||
wsList.push(legacyFormData.url);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// A video may legitimately declare more than one web seed (e.g. the primary CDN
|
||||
// plus an independent backup origin) so the WebTorrent swarm can pull pieces
|
||||
// from either when one is overloaded. Multiple `ws=` params are the standard
|
||||
// BEP19 carrier and playback already attaches every one of them, so the
|
||||
// authoring/normalization layer must preserve the full set — never collapse it
|
||||
// to a single seed.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { extractAllWebSeeds } from "../js/magnetShared.js";
|
||||
import { normalizeVideoNotePayload } from "../js/services/videoNotePayload.js";
|
||||
|
||||
const BTIH = "a".repeat(40);
|
||||
const MAGNET = `magnet:?xt=urn:btih:${BTIH}`;
|
||||
const SEED_A = "https://cdn-a.example.com/video.mp4";
|
||||
const SEED_B = "https://cdn-b.example.com/video.mp4";
|
||||
|
||||
test("extractAllWebSeeds returns every ws= in order, deduped", () => {
|
||||
const magnet = `${MAGNET}&ws=${encodeURIComponent(SEED_A)}&ws=${encodeURIComponent(
|
||||
SEED_B,
|
||||
)}&ws=${encodeURIComponent(SEED_A)}`;
|
||||
assert.deepEqual(extractAllWebSeeds(magnet), [SEED_A, SEED_B]);
|
||||
});
|
||||
|
||||
test("extractAllWebSeeds returns [] when there are no web seeds", () => {
|
||||
assert.deepEqual(extractAllWebSeeds(MAGNET), []);
|
||||
assert.deepEqual(extractAllWebSeeds(""), []);
|
||||
assert.deepEqual(extractAllWebSeeds(null), []);
|
||||
});
|
||||
|
||||
test("an array of web seeds all survive into the published magnet", () => {
|
||||
const { payload, errors } = normalizeVideoNotePayload({
|
||||
title: "Multi seed",
|
||||
magnet: MAGNET,
|
||||
ws: [SEED_A, SEED_B],
|
||||
});
|
||||
|
||||
assert.deepEqual(errors, []);
|
||||
const seeds = extractAllWebSeeds(payload.legacyFormData.magnet);
|
||||
assert.ok(seeds.includes(SEED_A), "first webseed must be carried in the magnet");
|
||||
assert.ok(seeds.includes(SEED_B), "second webseed must be carried in the magnet");
|
||||
});
|
||||
|
||||
test("a newline/comma-delimited ws field is split into multiple seeds", () => {
|
||||
const { payload } = normalizeVideoNotePayload({
|
||||
title: "Multi seed text",
|
||||
magnet: MAGNET,
|
||||
ws: `${SEED_A}\n${SEED_B}`,
|
||||
});
|
||||
|
||||
const seeds = extractAllWebSeeds(payload.legacyFormData.magnet);
|
||||
assert.ok(seeds.includes(SEED_A));
|
||||
assert.ok(seeds.includes(SEED_B));
|
||||
});
|
||||
|
||||
test("the hosted CDN url is added as a webseed alongside the explicit ones", () => {
|
||||
const cdn = "https://cdn.example.com/video.mp4";
|
||||
const { payload } = normalizeVideoNotePayload({
|
||||
title: "Cdn plus seed",
|
||||
url: cdn,
|
||||
magnet: MAGNET,
|
||||
ws: SEED_A,
|
||||
});
|
||||
|
||||
const seeds = extractAllWebSeeds(payload.legacyFormData.magnet);
|
||||
assert.ok(seeds.includes(SEED_A), "explicit webseed preserved");
|
||||
assert.ok(seeds.includes(cdn), "hosted url also attached as a webseed");
|
||||
});
|
||||
|
||||
test("a single webseed string still works (back-compat)", () => {
|
||||
const { payload } = normalizeVideoNotePayload({
|
||||
title: "Single seed",
|
||||
magnet: MAGNET,
|
||||
ws: SEED_A,
|
||||
});
|
||||
|
||||
const seeds = extractAllWebSeeds(payload.legacyFormData.magnet);
|
||||
assert.deepEqual(seeds, [SEED_A]);
|
||||
// The single content `ws` field still reflects the primary seed.
|
||||
assert.equal(payload.legacyFormData.ws, SEED_A);
|
||||
});
|
||||
|
||||
test("duplicate webseeds are not written twice", () => {
|
||||
const { payload } = normalizeVideoNotePayload({
|
||||
title: "Dupe seed",
|
||||
magnet: MAGNET,
|
||||
ws: [SEED_A, SEED_A, " " + SEED_A + " "],
|
||||
});
|
||||
|
||||
const seeds = extractAllWebSeeds(payload.legacyFormData.magnet);
|
||||
assert.equal(
|
||||
seeds.filter((s) => s === SEED_A).length,
|
||||
1,
|
||||
"the same webseed must appear at most once",
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user