// Browser-direct Blossom (BUD-02) upload: hash locally, sign the kind-24242 // authorization with the user's NIP-07 extension, PUT straight to the blossom // server. The file never transits podsteadr. import { nip07 } from './nip07'; export async function sha256File(file: File, onProgress?: (frac: number) => void): Promise { // crypto.subtle needs the whole buffer; fine for podcast-sized mp4s (~2 GB cap). const buf = await file.arrayBuffer(); onProgress?.(0.5); const digest = await crypto.subtle.digest('SHA-256', buf); onProgress?.(1); return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); } export interface BlossomUpload { sha256: string; size: number; url: string; } export async function uploadToBlossom( blossomUrl: string, file: File, sha256: string, onProgress?: (frac: number) => void, ): Promise { const now = Math.floor(Date.now() / 1000); const auth = await nip07().signEvent({ kind: 24242, created_at: now, content: `Upload ${file.name}`, tags: [ ['t', 'upload'], ['x', sha256], ['expiration', String(now + 600)], ], }); const base = blossomUrl.replace(/\/+$/, ''); // XHR instead of fetch for upload progress events. await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', `${base}/upload`); xhr.setRequestHeader('authorization', `Nostr ${btoa(JSON.stringify(auth))}`); xhr.setRequestHeader('content-type', file.type || 'video/mp4'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) onProgress?.(e.loaded / e.total); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) resolve(); else reject(new Error(`blossom upload failed: ${xhr.status} ${xhr.responseText.slice(0, 200)}`)); }; xhr.onerror = () => reject(new Error('blossom upload failed: network error')); xhr.send(file); }); return { sha256, size: file.size, url: `${base}/${sha256}` }; } /** Read the media duration (seconds) from a local file, for itunes:duration. */ export function probeDuration(file: File): Promise { return new Promise((resolve) => { const el = document.createElement('video'); el.preload = 'metadata'; el.onloadedmetadata = () => { URL.revokeObjectURL(el.src); resolve(Number.isFinite(el.duration) ? Math.round(el.duration) : null); }; el.onerror = () => { URL.revokeObjectURL(el.src); resolve(null); }; el.src = URL.createObjectURL(file); }); }