mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Fix magnet playback handling and update agent guidance
This commit is contained in:
1 parent
2442be4e77
commit
7742e3968c
3 files changed
+138
-59
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user