feat: improve magnet link reliability with webseeds

- Update `UploadModal.js` to pre-calculate R2 keys and public URLs before upload.
- Update `torrentHash.js` to embed the calculated video URL as a webseed (`url-list`) in the `.torrent` file.
- Update `r2Service.js` to accept forced keys/URLs and append `&xs=` (pointing to the .torrent file) to the magnet link.
- Ensures magnet links work for peerless downloads via WebSeed (BEP19) and HTTP Seeding (BEP17).
This commit is contained in:
google-labs-jules[bot] committed 2026-01-15 13:26:27 +00:00
1 parent f748f663c8
commit 4e840c6159
3 files changed
+68 -9

No files matched your search

+21 -5
View File
@@ -551,6 +551,10 @@ class R2Service {
settingsInput = null,
publishVideoNote,
onReset,
forcedVideoKey = "",
forcedVideoUrl = "",
forcedTorrentKey = "",
forcedTorrentUrl = "",
} = {}) {
if (settingsInput) {
const saved = await this.handleCloudflareSettingsSubmit(settingsInput, {
@@ -638,9 +642,12 @@ class R2Service {
let statusMessage = `Uploading to ${bucketEntry.bucket}…`;
this.setCloudflareUploadStatus(statusMessage, "info");
const key = buildR2Key(npub, file);
const publicUrl = buildPublicUrl(bucketEntry.publicBaseUrl, key);
// Use forced keys if provided, otherwise generate them
const key = forcedVideoKey || buildR2Key(npub, file);
const publicUrl = forcedVideoUrl || buildPublicUrl(bucketEntry.publicBaseUrl, key);
const buildTorrentKey = () => {
if (forcedTorrentKey) return forcedTorrentKey;
const baseKey = key.replace(/\.[^/.]+$/, "");
if (baseKey && baseKey !== key) {
return `${baseKey}.torrent`;
@@ -687,7 +694,7 @@ class R2Service {
});
let publishOutcome = true;
let torrentUrl = "";
let torrentUrl = forcedTorrentUrl || "";
if (torrentFile) {
this.setCloudflareUploadStatus("Uploading torrent metadata...", "info");
@@ -700,7 +707,9 @@ class R2Service {
file: torrentFile,
contentType: "application/x-bittorrent",
});
torrentUrl = buildPublicUrl(bucketEntry.publicBaseUrl, torrentKey);
if (!torrentUrl) {
torrentUrl = buildPublicUrl(bucketEntry.publicBaseUrl, torrentKey);
}
} catch (err) {
userLogger.warn("Torrent metadata upload failed, continuing...", err);
}
@@ -722,7 +731,14 @@ class R2Service {
if (hasValidInfoHash) {
const encodedDn = encodeURIComponent(file.name);
const encodedWs = encodeURIComponent(publicUrl);
generatedMagnet = `magnet:?xt=urn:btih:${normalizedInfoHash}&dn=${encodedDn}&ws=${encodedWs}`;
let magnet = `magnet:?xt=urn:btih:${normalizedInfoHash}&dn=${encodedDn}&ws=${encodedWs}`;
if (torrentUrl) {
const encodedXs = encodeURIComponent(torrentUrl);
magnet += `&xs=${encodedXs}`;
}
generatedMagnet = magnet;
generatedWs = publicUrl;
} else {
if (infoHash) {
+43 -1
View File
@@ -9,6 +9,7 @@ import {
} from "../../services/videoNotePayload.js";
import { createTorrentMetadata } from "../../utils/torrentHash.js";
import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
import { buildR2Key, buildPublicUrl } from "../../r2.js";
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
@@ -660,6 +661,39 @@ export class UploadModal {
const npub = this.safeEncodeNpub(pubkey);
// Pre-calculate Keys & URLs to support WebSeeding
// We need the Public URL *before* we generate the torrent so we can embed it.
let videoKey = "";
let videoPublicUrl = "";
let torrentKey = "";
let torrentPublicUrl = "";
try {
// Temporarily grab current bucket settings to calculate URL
const bucketName = this.inputs.r2BucketName?.value || sanitizeBucketName(npub);
// Note: The actual upload might re-verify this, but we need a best-guess for the torrent.
// We rely on r2Service using the *same* logic or accepting our forced keys.
// Since we can't easily get the 'final' bucket config here without doing the 'ensureBucket' dance first,
// we'll do a lightweight check of the settings form.
const currentSettings = this.collectSettingsForm();
if (!currentSettings.baseDomain) {
throw new Error("Missing Public URL (Base Domain) in settings.");
}
videoKey = buildR2Key(npub, file);
// 'buildPublicUrl' logic: ${baseDomain}/${key}
videoPublicUrl = buildPublicUrl(currentSettings.baseDomain, videoKey);
// For the torrent file, we swap extension
const baseKey = videoKey.replace(/\.[^/.]+$/, "");
torrentKey = (baseKey && baseKey !== videoKey) ? `${baseKey}.torrent` : `${videoKey}.torrent`;
torrentPublicUrl = buildPublicUrl(currentSettings.baseDomain, torrentKey);
} catch (prepErr) {
userLogger.warn("Failed to pre-calculate R2 keys:", prepErr);
// We continue, but WebSeed generation will fail or be skipped.
}
// 1. Calculate Torrent Info Hash (Client-side)
this.isUploading = true;
this.submitButton.disabled = true;
@@ -669,7 +703,10 @@ export class UploadModal {
let infoHash = "";
let torrentFile = null;
try {
const torrentMetadata = await createTorrentMetadata(file);
// Pass the pre-calculated video URL as a WebSeed
const urlList = videoPublicUrl ? [videoPublicUrl] : [];
const torrentMetadata = await createTorrentMetadata(file, urlList);
infoHash = torrentMetadata?.infoHash || "";
if (torrentMetadata?.torrentFile) {
const baseName = file.name.replace(/\.[^/.]+$/, "") || file.name;
@@ -715,6 +752,11 @@ export class UploadModal {
settingsInput: this.collectSettingsForm(),
publishVideoNote: this.publishVideoNote,
onReset: () => this.resetForm(),
// Pass forced keys/URLs to ensure what's in the torrent matches where we upload
forcedVideoKey: videoKey,
forcedVideoUrl: videoPublicUrl,
forcedTorrentKey: torrentKey,
forcedTorrentUrl: torrentPublicUrl,
});
}
+4 -3
View File
@@ -6,7 +6,7 @@ const TORRENT_SEED_OPTIONS = {
destroyStoreOnDestroy: true,
};
function seedTorrentForFile(file) {
function seedTorrentForFile(file, urlList = []) {
if (!file) {
return Promise.reject(new Error("No file provided for hashing."));
}
@@ -25,6 +25,7 @@ function seedTorrentForFile(file) {
{
...TORRENT_SEED_OPTIONS,
name: file.name,
urlList: urlList,
},
(torrent) => {
const infoHash = torrent.infoHash;
@@ -66,6 +67,6 @@ export async function calculateTorrentInfoHash(file) {
return infoHash;
}
export async function createTorrentMetadata(file) {
return seedTorrentForFile(file);
export async function createTorrentMetadata(file, urlList = []) {
return seedTorrentForFile(file, urlList);
}