mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
3 files changed
+96
-56
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user