diff --git a/components/upload-modal.html b/components/upload-modal.html
index a22e1bc3..1e7cb32d 100644
--- a/components/upload-modal.html
+++ b/components/upload-modal.html
@@ -209,12 +209,12 @@
-
Direct link to a hosted video file.
+
Primary hosted video source (URL-first playback).
-
Optional P2P fallback or primary source.
+
Optional WebTorrent fallback when the URL is unavailable.
diff --git a/js/services/r2Service.js b/js/services/r2Service.js
index 3055297f..5c289192 100644
--- a/js/services/r2Service.js
+++ b/js/services/r2Service.js
@@ -31,6 +31,15 @@ import {
} from "./videoNotePayload.js";
const STATUS_VARIANTS = new Set(["info", "success", "error", "warning"]);
+const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
+
+function normalizeInfoHash(value) {
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
+}
+
+function isValidInfoHash(value) {
+ return INFO_HASH_PATTERN.test(value);
+}
function createDefaultSettings() {
return {
@@ -187,10 +196,10 @@ class R2Service {
primaryVariant.m = mimeType;
}
- const normalizedHash =
- typeof infoHash === "string" ? infoHash.trim().toLowerCase() : "";
+ const normalizedHash = normalizeInfoHash(infoHash);
if (
normalizedHash &&
+ isValidInfoHash(normalizedHash) &&
(!primaryVariant.x || !String(primaryVariant.x).trim())
) {
primaryVariant.x = normalizedHash;
@@ -705,14 +714,27 @@ class R2Service {
} else {
// Construct the Magnet Link
// magnet:?xt=urn:btih:&dn=&ws=
+ const normalizedInfoHash = normalizeInfoHash(infoHash);
+ const hasValidInfoHash = isValidInfoHash(normalizedInfoHash);
let generatedMagnet = "";
let generatedWs = "";
- if (infoHash) {
- const encodedDn = encodeURIComponent(file.name);
- const encodedWs = encodeURIComponent(publicUrl);
- generatedMagnet = `magnet:?xt=urn:btih:${infoHash}&dn=${encodedDn}&ws=${encodedWs}`;
- generatedWs = publicUrl;
+ if (hasValidInfoHash) {
+ const encodedDn = encodeURIComponent(file.name);
+ const encodedWs = encodeURIComponent(publicUrl);
+ generatedMagnet = `magnet:?xt=urn:btih:${normalizedInfoHash}&dn=${encodedDn}&ws=${encodedWs}`;
+ generatedWs = publicUrl;
+ } else {
+ if (infoHash) {
+ userLogger.warn(
+ "Invalid info hash provided. Skipping magnet and webseed generation.",
+ infoHash
+ );
+ }
+ this.setCloudflareUploadStatus(
+ "Info hash missing or invalid. Publishing URL-first without WebTorrent fallback.",
+ "warning"
+ );
}
const rawVideoPayload = {
@@ -731,7 +753,7 @@ class R2Service {
const mergedNip71 = this.buildNip71MetadataForUpload(metadata?.nip71, {
publicUrl,
file,
- infoHash,
+ infoHash: hasValidInfoHash ? normalizedInfoHash : "",
});
if (mergedNip71 && Object.keys(mergedNip71).length) {
rawVideoPayload.nip71 = mergedNip71;
diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js
index 642553ae..06c0d0cf 100644
--- a/js/ui/components/UploadModal.js
+++ b/js/ui/components/UploadModal.js
@@ -10,6 +10,16 @@ import {
import { createTorrentMetadata } from "../../utils/torrentHash.js";
import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
+const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
+
+function normalizeInfoHash(value) {
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
+}
+
+function isValidInfoHash(value) {
+ return INFO_HASH_PATTERN.test(value);
+}
+
export class UploadModal {
constructor({
authService,
@@ -671,6 +681,29 @@ export class UploadModal {
userLogger.warn("Failed to calculate info hash:", hashErr);
}
+ const normalizedInfoHash = normalizeInfoHash(infoHash);
+ const hasValidInfoHash = isValidInfoHash(normalizedInfoHash);
+
+ if (!hasValidInfoHash) {
+ const proceed = confirm(
+ "We couldn't calculate a valid info hash. Publishing will continue with URL-first playback only, and WebTorrent fallback will be unavailable. Continue?"
+ );
+ if (!proceed) {
+ this.updateUploadStatus(
+ "Upload canceled. A valid info hash is required for WebTorrent fallback.",
+ "warning"
+ );
+ this.isUploading = false;
+ this.submitButton.disabled = false;
+ this.updateProgress(null);
+ return;
+ }
+ this.updateUploadStatus(
+ "Continuing without WebTorrent fallback (info hash unavailable).",
+ "warning"
+ );
+ }
+
// 2. Upload
await this.r2Service.uploadVideo({
npub,
@@ -678,7 +711,7 @@ export class UploadModal {
thumbnailFile,
torrentFile,
metadata,
- infoHash,
+ infoHash: hasValidInfoHash ? normalizedInfoHash : "",
settingsInput: this.collectSettingsForm(),
publishVideoNote: this.publishVideoNote,
onReset: () => this.resetForm(),