From 6262a3cb3750b538dceba780b229a64536bca707 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:28:57 -0500 Subject: [PATCH] Add torrent metadata upload for R2 videos --- js/services/r2Service.js | 28 ++++++++- js/ui/components/UploadModal.js | 17 +++-- js/utils/torrentHash.js | 107 +++++++++++++++++--------------- 3 files changed, 96 insertions(+), 56 deletions(-) diff --git a/js/services/r2Service.js b/js/services/r2Service.js index c5345e50..3055297f 100644 --- a/js/services/r2Service.js +++ b/js/services/r2Service.js @@ -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, diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index e84e0ff5..642553ae 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -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(), diff --git a/js/utils/torrentHash.js b/js/utils/torrentHash.js index b7b715c8..02c1cd60 100644 --- a/js/utils/torrentHash.js +++ b/js/utils/torrentHash.js @@ -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} - 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); }