Merge pull request #1230 from PR0M3TH3AN/codex/add-torrent-file-generation-and-upload

Upload .torrent metadata to R2 and publish xs hint for video posts
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-14 21:31:43 -05:00
commit f52da2581b
3 files changed
+96 -56

No files matched your search

+27 -1
View File
@@ -536,6 +536,7 @@ class R2Service {
npub = "",
file = null,
thumbnailFile = null,
torrentFile = null,
metadata = {},
infoHash = "",
settingsInput = null,
@@ -630,6 +631,13 @@ class R2Service {
const key = buildR2Key(npub, file);
const publicUrl = buildPublicUrl(bucketEntry.publicBaseUrl, key);
const buildTorrentKey = () => {
const baseKey = key.replace(/\.[^/.]+$/, "");
if (baseKey && baseKey !== key) {
return `${baseKey}.torrent`;
}
return `${key}.torrent`;
};
try {
const s3 = makeR2Client({ accountId, accessKeyId, secretAccessKey });
@@ -670,6 +678,24 @@ class R2Service {
});
let publishOutcome = true;
let torrentUrl = "";
if (torrentFile) {
this.setCloudflareUploadStatus("Uploading torrent metadata...", "info");
const torrentKey = buildTorrentKey();
try {
await multipartUpload({
s3,
bucket: bucketEntry.bucket,
key: torrentKey,
file: torrentFile,
contentType: "application/x-bittorrent",
});
torrentUrl = buildPublicUrl(bucketEntry.publicBaseUrl, torrentKey);
} catch (err) {
userLogger.warn("Torrent metadata upload failed, continuing...", err);
}
}
if (typeof publishVideoNote !== "function") {
userLogger.warn(
@@ -696,7 +722,7 @@ class R2Service {
thumbnail: metadata?.thumbnail ?? "",
description: metadata?.description ?? "",
ws: generatedWs || (metadata?.ws ?? ""),
xs: metadata?.xs ?? "",
xs: torrentUrl || (metadata?.xs ?? ""),
enableComments: metadata?.enableComments,
isNsfw: metadata?.isNsfw,
isForKids: metadata?.isForKids,
+11 -6
View File
@@ -7,7 +7,7 @@ import {
normalizeVideoNotePayload,
VIDEO_NOTE_ERROR_CODES,
} from "../../services/videoNotePayload.js";
import { calculateTorrentInfoHash } from "../../utils/torrentHash.js";
import { createTorrentMetadata } from "../../utils/torrentHash.js";
import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
export class UploadModal {
@@ -657,14 +657,18 @@ export class UploadModal {
this.updateProgress(0); // Show bar at 0
let infoHash = "";
let torrentFile = null;
try {
infoHash = await calculateTorrentInfoHash(file);
const torrentMetadata = await createTorrentMetadata(file);
infoHash = torrentMetadata?.infoHash || "";
if (torrentMetadata?.torrentFile) {
const baseName = file.name.replace(/\.[^/.]+$/, "") || file.name;
torrentFile = new File([torrentMetadata.torrentFile], `${baseName}.torrent`, {
type: "application/x-bittorrent",
});
}
} catch (hashErr) {
userLogger.warn("Failed to calculate info hash:", hashErr);
// We can proceed without it, or fail. The plan says we need it.
// Let's warn but proceed? Or fail? The user said "Also lets not forget that we calculate a torrent hash".
// I will proceed but log it, maybe the upload handles it gracefully (missing magnet).
// But passing empty infoHash means no magnet link generated in R2Service.
}
// 2. Upload
@@ -672,6 +676,7 @@ export class UploadModal {
npub,
file,
thumbnailFile,
torrentFile,
metadata,
infoHash,
settingsInput: this.collectSettingsForm(),
+58 -49
View File
@@ -1,6 +1,59 @@
import WebTorrent from "../webtorrent.min.js";
import { userLogger } from "./logger.js";
const TORRENT_SEED_OPTIONS = {
announce: [],
destroyStoreOnDestroy: true,
};
function seedTorrentForFile(file) {
if (!file) {
return Promise.reject(new Error("No file provided for hashing."));
}
const client = new WebTorrent({
tracker: false,
dht: false,
lsd: false,
webSeeds: false,
});
return new Promise((resolve, reject) => {
try {
client.seed(
file,
{
...TORRENT_SEED_OPTIONS,
name: file.name,
},
(torrent) => {
const infoHash = torrent.infoHash;
const torrentFile = torrent.torrentFile || torrent.torrentFileBuffer || null;
client.destroy((err) => {
if (err) {
userLogger.warn("Error destroying temporary hashing client:", err);
}
if (infoHash) {
resolve({ infoHash, torrentFile });
} else {
reject(new Error("Failed to calculate info hash."));
}
});
}
);
client.on("error", (err) => {
userLogger.error("WebTorrent client error during hashing:", err);
client.destroy();
reject(err);
});
} catch (err) {
client.destroy();
reject(err);
}
});
}
/**
* Calculates the torrent infoHash for a given file client-side.
* This operation requires reading and hashing the entire file, so it may take time for large files.
@@ -9,54 +62,10 @@ import { userLogger } from "./logger.js";
* @returns {Promise<string>} - Resolves with the hex encoded infoHash.
*/
export async function calculateTorrentInfoHash(file) {
if (!file) {
throw new Error("No file provided for hashing.");
}
const { infoHash } = await seedTorrentForFile(file);
return infoHash;
}
// Create a temporary client just for hashing.
// We disable trackers and dht/lsd to keep it quiet and fast (local only).
const client = new WebTorrent({
tracker: false,
dht: false,
lsd: false,
webSeeds: false,
});
return new Promise((resolve, reject) => {
// Seed the file. This calculates the infoHash.
// We use destroyStoreOnDestroy: true to clean up memory/storage afterwards.
try {
client.seed(
file,
{
announce: [], // No trackers
destroyStoreOnDestroy: true,
name: file.name,
},
(torrent) => {
const infoHash = torrent.infoHash;
// Destroy the client and the torrent data immediately.
client.destroy((err) => {
if (err) {
userLogger.warn("Error destroying temporary hashing client:", err);
}
if (infoHash) {
resolve(infoHash);
} else {
reject(new Error("Failed to calculate info hash."));
}
});
}
);
client.on("error", (err) => {
userLogger.error("WebTorrent client error during hashing:", err);
client.destroy();
reject(err);
});
} catch (err) {
client.destroy();
reject(err);
}
});
export async function createTorrentMetadata(file) {
return seedTorrentForFile(file);
}