creates video voice memos from audio clips; with bluesky integration. trill.ptr.pet

fix: use recommended video uploading method

ptr.pet 7dbb8d96 edcab5cd

verified
Changed files
+80 -5
src
+79 -4
src/lib/at.ts
··· 25 25 if (!didDoc.ok) throw didDoc.data.error; 26 26 return { 27 27 client: rpc, 28 + agent, 28 29 did: res.data.did, 29 30 handle: res.data.handle, 30 31 pds: didDoc.data.pds, ··· 38 39 altText?: string, 39 40 ) => { 40 41 const login = await getSessionClient(did); 41 - const upload = await login.client.post("com.atproto.repo.uploadBlob", { 42 - input: blob, 42 + 43 + const serviceAuthUrl = new URL( 44 + `${login.pds}/xrpc/com.atproto.server.getServiceAuth`, 45 + ); 46 + serviceAuthUrl.searchParams.append( 47 + "aud", 48 + login.pds!.replace("https://", "did:web:"), 49 + ); 50 + serviceAuthUrl.searchParams.append("lxm", "com.atproto.repo.uploadBlob"); 51 + serviceAuthUrl.searchParams.append( 52 + "exp", 53 + (Math.floor(Date.now() / 1000) + 60 * 30).toString(), 54 + ); // 30 minutes 55 + 56 + const serviceAuthResponse = await login.agent.handle( 57 + `${serviceAuthUrl.pathname}${serviceAuthUrl.search}`, 58 + { 59 + method: "GET", 60 + }, 61 + ); 62 + 63 + if (!serviceAuthResponse.ok) { 64 + const error = await serviceAuthResponse.text(); 65 + throw `failed to get service auth: ${error}`; 66 + } 67 + 68 + const serviceAuth = await serviceAuthResponse.json(); 69 + const token = serviceAuth.token; 70 + 71 + const uploadUrl = new URL( 72 + "https://video.bsky.app/xrpc/app.bsky.video.uploadVideo", 73 + ); 74 + uploadUrl.searchParams.append("did", did); 75 + uploadUrl.searchParams.append("name", "video.mp4"); 76 + 77 + const uploadResponse = await fetch(uploadUrl.toString(), { 78 + method: "POST", 79 + headers: { 80 + Authorization: `Bearer ${token}`, 81 + "Content-Type": "video/mp4", 82 + }, 83 + body: blob, 43 84 }); 44 - if (!upload.ok) throw `failed to upload blob: ${upload.data.error}`; 85 + 86 + if (!uploadResponse.ok) { 87 + const error = await uploadResponse.text(); 88 + throw `failed to upload video: ${error}`; 89 + } 90 + 91 + const jobStatus = await uploadResponse.json(); 92 + let videoBlobRef = jobStatus.blob; 93 + 94 + while (!videoBlobRef) { 95 + await new Promise((resolve) => setTimeout(resolve, 1000)); 96 + 97 + const statusResponse = await fetch( 98 + `https://video.bsky.app/xrpc/app.bsky.video.getJobStatus?jobId=${jobStatus.jobId}`, 99 + ); 100 + 101 + if (!statusResponse.ok) { 102 + const error = await statusResponse.json(); 103 + if (error.error === "already_exists" && error.blob) { 104 + videoBlobRef = error.blob; 105 + break; 106 + } 107 + throw `failed to get job status: ${error.message || error.error}`; 108 + } 109 + 110 + const status = await statusResponse.json(); 111 + if (status.jobStatus.blob) { 112 + videoBlobRef = status.jobStatus.blob; 113 + } else if (status.jobStatus.state === "JOB_STATE_FAILED") { 114 + throw `video processing failed: ${status.jobStatus.error || "unknown error"}`; 115 + } 116 + } 117 + 45 118 const record: AppBskyFeedPost.Main = { 46 119 $type: "app.bsky.feed.post", 47 120 text: postContent, 48 121 embed: { 49 122 $type: "app.bsky.embed.video", 50 - video: upload.data.blob, 123 + video: videoBlobRef, 51 124 alt: altText, 52 125 }, 53 126 createdAt: new Date().toISOString(), 54 127 }; 128 + 55 129 const result = await login.client.post("com.atproto.repo.createRecord", { 56 130 input: { 57 131 collection: "app.bsky.feed.post", ··· 59 133 repo: did, 60 134 }, 61 135 }); 136 + 62 137 if (!result.ok) throw `failed to upload post: ${result.data.error}`; 63 138 return result.data; 64 139 };
+1 -1
src/lib/oauthMetadata.json
··· 4 4 "client_uri": "http://localhost:3000", 5 5 "logo_uri": "http://localhost:3000/favicon.png", 6 6 "redirect_uris": ["http://127.0.0.1:3000/"], 7 - "scope": "atproto repo:app.bsky.feed.post?action=create blob:video/*", 7 + "scope": "atproto repo:app.bsky.feed.post?action=create rpc:com.atproto.repo.uploadBlob?aud=* blob:video/*", 8 8 "grant_types": ["authorization_code", "refresh_token"], 9 9 "response_types": ["code"], 10 10 "token_endpoint_auth_method": "none",