import { AppBskyEmbedVideo, AppBskyVideoDefs, AtpAgent, BlobRef, } from "npm:@atproto/api"; const userAgent = new AtpAgent({ service: prompt("Service URL (default: https://bsky.social):") || "https://bsky.social", }); await userAgent.login({ identifier: prompt("Handle:")!, password: prompt("Password:")!, }); console.log(`Logged in as ${userAgent.session?.handle}`); const videoPath = prompt("Video file (.mp4):")!; const { data: serviceAuth } = await userAgent.com.atproto.server.getServiceAuth( { aud: `did:web:${userAgent.dispatchUrl.host}`, lxm: "com.atproto.repo.uploadBlob", exp: Date.now() / 1000 + 60 * 30, // 30 minutes }, ); const token = serviceAuth.token; const file = await Deno.open(videoPath); const { size } = await file.stat(); // optional: print upload progress let bytesUploaded = 0; const progressTrackingStream = new TransformStream({ transform(chunk, controller) { controller.enqueue(chunk); bytesUploaded += chunk.byteLength; console.log( "upload progress:", Math.trunc(bytesUploaded / size * 100) + "%", ); }, flush() { console.log("upload complete ✨"); }, }); const uploadUrl = new URL( "https://video.bsky.app/xrpc/app.bsky.video.uploadVideo", ); uploadUrl.searchParams.append("did", userAgent.session!.did); uploadUrl.searchParams.append("name", videoPath.split("/").pop()!); const uploadResponse = await fetch(uploadUrl, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "video/mp4", "Content-Length": String(size), }, body: file.readable.pipeThrough(progressTrackingStream), }); const jobStatus = (await uploadResponse.json()) as AppBskyVideoDefs.JobStatus; console.log("JobId:", jobStatus.jobId); let blob: BlobRef | undefined = jobStatus.blob; const videoAgent = new AtpAgent({ service: "https://video.bsky.app" }); while (!blob) { const { data: status } = await videoAgent.app.bsky.video.getJobStatus( { jobId: jobStatus.jobId }, ); console.log( "Status:", status.jobStatus.state, status.jobStatus.progress || "", ); if (status.jobStatus.blob) { blob = status.jobStatus.blob; } // wait a second await new Promise((resolve) => setTimeout(resolve, 1000)); } console.log("posting..."); await userAgent.post({ text: "This post should have a video attached", langs: ["en"], embed: { $type: "app.bsky.embed.video", video: blob, aspectRatio: await getAspectRatio(videoPath), } satisfies AppBskyEmbedVideo.Main, }); console.log("done ✨"); // bonus: get aspect ratio using ffprobe // in the browser, you can just put the video uri in a