Fix magnet playback handling and update agent guidance

This commit is contained in:
thePR0M3TH3AN committed 2025-09-24 13:07:14 -04:00
1 parent 2442be4e77
commit 7742e3968c
3 files changed
+138 -59

No files matched your search

+53 -21
View File
@@ -91,10 +91,17 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
## 5) Key Client Functions (reference)
### 5.1 `augmentMagnet(raw, ws?, xs?)`
### 5.1 `normalizeAndAugmentMagnet(raw, { webSeed?, torrentUrl?, xs?, extraTrackers? })`
* Accepts a magnet URI and conditionally **appends** `ws` and `xs` query params.
* Returns the same magnet if not a `magnet:` scheme.
* Accepts a magnet URI or bare hex info hash and returns a **sanitized magnet**.
* Fixes accidentally encoded `xt=urn%3Abtih:` segments and tolerates bare hashes by
promoting them to `magnet:?xt=urn:btih:<hash>`.
* Appends only **browser-safe WSS trackers** plus optional extras, avoiding mixed
content.
* Adds HTTPS web seeds (`ws=`) and torrent hints (`xs=`) when provided, skipping
insecure `http://` web seeds unless the app itself is served via HTTP.
* Preserves existing parameters in their original form—no `new URL()` usage and no
percent re-encoding of the `xt` payload.
### 5.2 `probeUrl(url)`
@@ -120,12 +127,36 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
* **Cards:** add both attributes where applicable:
* `data-play-url="<encoded URL or empty>"`
* `data-play-magnet="<magnet or empty>"`
* **Click delegate:** look for a closest element bearing either attribute; decode `data-play-url` prior to use.
* `data-play-magnet="<raw magnet or empty>"`
* **Click delegate:** look for a closest element bearing either attribute; decode
both `data-play-url` and `data-play-magnet` (if encoded upstream) **before** passing
them to playback logic. Never double-encode magnets when writing them into the DOM.
---
## 7) Server Requirements for Hosted URLs
## 7) Main vs Unstable — legacy WebTorrent compatibility lessons
Legacy Bitvid posts (v1 / magnet-only) still play on **Main**. They broke on
**Unstable** because of double-encoded magnets, URL constructors, and strict
filters. Apply these rules to stay compatible:
* **Raw magnets end-to-end.** Store the literal `magnet:?xt=urn:btih:...` in the DOM
and feed that same string into WebTorrent. If you must encode for transport, be
sure to decode once when handling clicks.
* **No `new URL()` for magnets.** Appending params with `URLSearchParams` or `new URL`
percent-encodes the `xt` value and can corrupt legacy hashes. When you need to add
trackers or cache-busting params, concatenate plain text (or use
`normalizeAndAugmentMagnet`, which already preserves the payload).
* **Converters should allow magnet-only posts.** A note is playable when it exposes
either a usable `magnet` **or** a direct `url`. Don’t require `version >= 2` to keep
legacy uploads alive.
* **Browser-safe announce list.** Ship WSS trackers for browser playback. Skip UDP
trackers in the client path and only add `ws=` hints when they’re HTTPS on HTTPS
pages (mixed content gets blocked anyway).
---
## 8) Server Requirements for Hosted URLs
* **CORS:** allow the app origin (or `*`).
* **Range requests:** ensure `Accept-Ranges: bytes`.
@@ -134,7 +165,7 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
---
## 8) P2P Notes for Agents
## 9) P2P Notes for Agents
* **`ws=` (web seed):** HTTP(S) base path where the file is fetchable; enables WebTorrent to download pieces even with zero peers.
* **`xs=`:** HTTP(S) URL of the `.torrent` metadata so the client can start promptly without DHT/peer discovery.
@@ -142,7 +173,7 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
---
## 9) Interop & Extensions
## 10) Interop & Extensions
* **Live streams:** future option via HLS URLs (MIME `application/x-mpegURL`). For Nostr live presence, consider **NIP‑53** events alongside VOD 1063 entries.
* **Adaptive Bitrate + P2P:** if ABR is needed with P2P, consider a segment‑sharing overlay (e.g., p2p segment loaders) rather than WebTorrent.
@@ -150,7 +181,7 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
---
## 10) Testing Checklist (agents)
## 11) Testing Checklist (agents)
* **Form validation:** title present; ensure `(url || magnet)` is true; `ws` and `xs` appended only for magnet.
* **Probe:** healthy URL returns `ok=true`; dead URL hides/badges card; cache prevents re‑probing every render.
@@ -161,7 +192,7 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
---
## 11) Guardrails & Do/Don’t
## 12) Guardrails & Do/Don’t
**Do**
@@ -177,7 +208,7 @@ This document tells AI agents how Bitvid works **now** and how to extend it goin
---
## 12) Quick Reference (snippets)
## 13) Quick Reference (snippets)
**Bitvid content object (30078)**
@@ -193,17 +224,18 @@ const content = {
};
```
**Magnet augmentation**
**Magnet normalization & augmentation**
```js
function augmentMagnet(raw, ws, xs) {
if (!raw) return "";
const u = new URL(raw);
if (u.protocol !== "magnet:") return raw;
if (ws) u.searchParams.append("ws", ws);
if (xs) u.searchParams.append("xs", xs);
return u.toString();
}
import { normalizeAndAugmentMagnet } from "./js/magnetUtils.js";
const { magnet } = normalizeAndAugmentMagnet(inputMagnetOrInfoHash, {
webSeed: hostedUrl ? [hostedUrl] : [],
torrentUrl: xsHint,
extraTrackers: ["wss://tracker.openwebtorrent.com"],
});
// Feed `magnet` directly to playVideoWithFallback / WebTorrent.
```
**Playback with fallback**
@@ -218,7 +250,7 @@ async function playVideoWithFallback({ url, magnet }) {
---
## 13) Roadmap Hooks (where to extend)
## 14) Roadmap Hooks (where to extend)
* **Analytics:** Keep using your existing 30078 analytics/logging. Add fields for `play_source: "url"|"torrent"` to measure cost savings.
* **Integrity:** Optionally compute SHA‑256 of the hosted file (off‑thread) and publish as NIP‑94 `["x", hash]` tag.
+13 -8
View File
@@ -2042,16 +2042,21 @@ class bitvidApp {
);
}
let cacheBustedMagnet = trimmedCandidate;
try {
const parsed = new URL(trimmedCandidate);
parsed.searchParams.set("ts", Date.now().toString());
cacheBustedMagnet = parsed.toString();
} catch (err) {
const separator = trimmedCandidate.includes("?") ? "&" : "?";
cacheBustedMagnet = `${trimmedCandidate}${separator}ts=${Date.now()}`;
const timestamp = Date.now().toString();
const [magnetPrefix, magnetQuery = ""] = trimmedCandidate.split("?", 2);
let normalizedMagnet = magnetPrefix;
let queryParts = magnetQuery
.split("&")
.map((part) => part.trim())
.filter((part) => part && !/^ts=\d+$/.test(part));
if (queryParts.length) {
normalizedMagnet = `${magnetPrefix}?${queryParts.join("&")}`;
}
const separator = normalizedMagnet.includes("?") ? "&" : "?";
const cacheBustedMagnet = `${normalizedMagnet}${separator}ts=${timestamp}`;
await torrentClient.cleanup();
this.resetTorrentStats();
+72 -30
View File
@@ -108,27 +108,69 @@ export function normalizeAndAugmentMagnet(
didMutate = true;
}
const hashIndex = working.indexOf("#");
let fragment = "";
if (hashIndex !== -1) {
fragment = working.slice(hashIndex);
working = working.slice(0, hashIndex);
}
const decodedXt = working.replace(ENCODED_BTih_PATTERN, (_, hash) => {
didMutate = true;
return `xt=${BTIH_PREFIX}${hash}`;
});
working = decodedXt;
let parsed;
try {
parsed = new URL(working);
} catch (err) {
if (!/^magnet:/i.test(working)) {
return {
magnet: working,
didChange: didMutate || working !== initial,
};
}
if (parsed.protocol !== "magnet:") {
return {
magnet: working,
didChange: didMutate || working !== initial,
};
const [schemePart, queryPart = ""] = working.split("?", 2);
const normalizedScheme = "magnet:";
if (schemePart !== normalizedScheme) {
didMutate = true;
}
const rawParams = queryPart
.split("&")
.map((part) => part.trim())
.filter(Boolean);
const params = [];
const decodeLoose = (value) => {
if (typeof value !== "string" || !value) {
return "";
}
try {
return decodeURIComponent(value);
} catch (err) {
return value;
}
};
for (const rawParam of rawParams) {
const [rawKey, rawValue = ""] = rawParam.split("=", 2);
const key = rawKey.trim();
if (!key) {
continue;
}
let value = rawValue.trim();
if (key.toLowerCase() === "xt" && value) {
const decoded = decodeLoose(value);
if (decoded !== value) {
value = decoded;
didMutate = true;
}
}
params.push({
key,
value,
decoded: decodeLoose(value),
});
}
const torrentHint = typeof torrentUrl === "string" && torrentUrl.trim()
@@ -151,9 +193,9 @@ export function normalizeAndAugmentMagnet(
: "https:";
const existingTrackers = new Set(
parsed.searchParams
.getAll("tr")
.map((value) => normalizeForComparison(value))
params
.filter((param) => param.key.toLowerCase() === "tr")
.map((param) => normalizeForComparison(param.decoded))
.filter(Boolean)
);
@@ -177,30 +219,34 @@ export function normalizeAndAugmentMagnet(
if (!normalizedTracker || existingTrackers.has(normalizedTracker)) {
continue;
}
parsed.searchParams.append("tr", trimmedTracker);
params.push({ key: "tr", value: trimmedTracker, decoded: trimmedTracker });
existingTrackers.add(normalizedTracker);
didMutate = true;
}
if (safeTorrentHint) {
const existingXs = new Set(
parsed.searchParams
.getAll("xs")
.map((value) => normalizeForComparison(value))
params
.filter((param) => param.key.toLowerCase() === "xs")
.map((param) => normalizeForComparison(param.decoded))
.filter(Boolean)
);
const normalizedXs = normalizeForComparison(safeTorrentHint);
if (normalizedXs && !existingXs.has(normalizedXs)) {
parsed.searchParams.append("xs", safeTorrentHint);
params.push({
key: "xs",
value: safeTorrentHint,
decoded: safeTorrentHint,
});
didMutate = true;
}
}
if (seedInputs.length) {
const existingWs = new Set(
parsed.searchParams
.getAll("ws")
.map((value) => normalizeForComparison(value))
params
.filter((param) => param.key.toLowerCase() === "ws")
.map((param) => normalizeForComparison(param.decoded))
.filter(Boolean)
);
for (const seedInput of seedInputs) {
@@ -222,7 +268,7 @@ export function normalizeAndAugmentMagnet(
const seedValue = parsedSeed.toString();
const normalizedSeed = normalizeForComparison(seedValue);
if (normalizedSeed && !existingWs.has(normalizedSeed)) {
parsed.searchParams.append("ws", seedValue);
params.push({ key: "ws", value: seedValue, decoded: seedValue });
existingWs.add(normalizedSeed);
didMutate = true;
}
@@ -237,15 +283,11 @@ export function normalizeAndAugmentMagnet(
}
}
let finalMagnet = parsed.toString();
const decodedFinalMagnet = finalMagnet.replace(
ENCODED_BTih_PATTERN,
(_, hash) => `xt=${BTIH_PREFIX}${hash}`
);
if (decodedFinalMagnet !== finalMagnet) {
finalMagnet = decodedFinalMagnet;
didMutate = true;
}
const queryString = params
.map(({ key, value }) => (value ? `${key}=${value}` : key))
.join("&");
const finalMagnet = `${normalizedScheme}${queryString ? `?${queryString}` : ""}${fragment}`;
return {
magnet: finalMagnet,