Music streaming on ATProto!
1import { 2 Client, 3 CredentialManager, 4 ok, 5 simpleFetchHandler, 6} from "@atcute/client"; 7 8import type {} from "@atcute/bluesky"; 9import type { ComAtprotoRepoApplyWrites } from "@atcute/atproto"; 10import { 11 ShCometV0FeedPlaylist, 12 ShCometV0FeedPlaylistTrack, 13 ShCometV0FeedTrack, 14} from "@comet/lexicons"; 15import type { ResourceUri } from "@atcute/lexicons"; 16import { splitEvery } from "rambdax"; 17 18// const manager = new CredentialManager({ service: "https://pds.ovy.sh" }); 19const manager = new CredentialManager({ service: "https://bsky.social" }); 20const rpc = new Client({ handler: manager }); 21 22interface Type { 23 $type: `${string}.${string}.${string}`; 24 [key: string]: any; 25} 26 27const createRecord = <T extends Type>(record: T) => 28 ok( 29 rpc.post("com.atproto.repo.createRecord", { 30 input: { collection: record.$type, repo: manager.session!.did, record }, 31 }), 32 ); 33 34await manager.login({ 35 identifier: Bun.env.COMET_TEST_IDENT!, 36 password: Bun.env.COMET_TEST_PASSWORD!, 37}); 38 39/** Upload a test audio blob. */ 40const uploadAudio = async () => { 41 const inputAudio = Bun.file("./test-track.opus"); 42 const { blob: audio } = await ok( 43 rpc.post("com.atproto.repo.uploadBlob", { input: inputAudio }), 44 ); 45 console.log(audio); 46}; 47 48/** Create a test track record. */ 49const createTrack = async () => { 50 const audio = { 51 $type: "blob", 52 ref: { 53 $link: "bafkreifiu63dr52dxzrurnspha5xvzlzqkho3hdzdhu6zvthrrvdpd6yve", 54 }, 55 mimeType: "audio/opus", 56 size: 3349806, 57 } as const; 58 59 const track: ShCometV0FeedTrack.Main = { 60 $type: "sh.comet.v0.feed.track", 61 audio, 62 title: "Testing Track 6", 63 createdAt: new Date().toJSON(), 64 }; 65 66 const response = await createRecord(track); 67 console.log(response); 68}; 69 70/** Create a test playlist */ 71const createPlaylist = async () => { 72 const playlistRecord: ShCometV0FeedPlaylist.Main = { 73 $type: "sh.comet.v0.feed.playlist", 74 title: "Testing Playlist", 75 type: "sh.comet.v0.feed.playlist#playlist", 76 createdAt: new Date().toJSON(), 77 tags: ["testing", "music"], 78 }; 79 80 const { uri: playlist } = await createRecord(playlistRecord); 81 console.log("created playlist", playlist); 82 83 const collection = "sh.comet.v0.feed.playlistTrack"; 84 const tracks = [ 85 "at://did:plc:jrrhosrfzgjf6v4oydav6ftb/sh.comet.v0.feed.track/3lpq2gsib2s2e", 86 "at://did:plc:jrrhosrfzgjf6v4oydav6ftb/sh.comet.v0.feed.track/3lpq2muqtnu2w", 87 "at://did:plc:jrrhosrfzgjf6v4oydav6ftb/sh.comet.v0.feed.track/3lpq2njjm6p2y", 88 "at://did:plc:jrrhosrfzgjf6v4oydav6ftb/sh.comet.v0.feed.track/3lpq2nrehj52o", 89 "at://did:plc:jrrhosrfzgjf6v4oydav6ftb/sh.comet.v0.feed.track/3lpq2nnacyg23", 90 ] as ResourceUri[]; 91 92 const created = await ok( 93 rpc.post("com.atproto.repo.applyWrites", { 94 input: { 95 repo: manager.session!.did, 96 writes: tracks.map( 97 (track, position) => 98 ({ 99 $type: "com.atproto.repo.applyWrites#create", 100 collection, 101 value: { 102 $type: collection, 103 playlist, 104 track, 105 position, 106 } satisfies ShCometV0FeedPlaylistTrack.Main, 107 }) satisfies ComAtprotoRepoApplyWrites.Create, 108 ), 109 }, 110 }), 111 ); 112 113 console.log(created); 114 console.log("created playlist tracks"); 115}; 116 117/** Create a veeeeery large test playlist. */ 118const createLargePlaylist = async () => { 119 const playlistRecord: ShCometV0FeedPlaylist.Main = { 120 $type: "sh.comet.v0.feed.playlist", 121 title: "Very lorge playlist", 122 type: "sh.comet.v0.feed.playlist#compilation", 123 createdAt: new Date().toJSON(), 124 }; 125 126 const { uri: playlist } = await createRecord(playlistRecord); 127 console.log("created playlist", playlist); 128 129 const collection = "sh.comet.v0.feed.playlistTrack"; 130 const tracks = new Array(2500) 131 .fill( 132 "at://did:plc:jrrhosrfzgjf6v4oydav6ftb/sh.comet.v0.feed.track/3lpq2gsib2s2e" as ResourceUri, 133 ) 134 .map( 135 (track, position) => 136 ({ 137 $type: "com.atproto.repo.applyWrites#create", 138 collection, 139 value: { 140 $type: collection, 141 playlist, 142 track, 143 position, 144 } satisfies ShCometV0FeedPlaylistTrack.Main, 145 }) satisfies ComAtprotoRepoApplyWrites.Create, 146 ); 147 148 for (const chunk of splitEvery(100, tracks)) { 149 // TODO: don't hit ratelimit 150 await ok( 151 rpc.post("com.atproto.repo.applyWrites", { 152 input: { 153 repo: manager.session!.did, 154 writes: chunk, 155 }, 156 }), 157 ); 158 console.log("wrote chunk"); 159 } 160 161 console.log("created playlist tracks"); 162}; 163 164// const testQuery = async () => { 165// const x = await ok(rpc.get("sh.comet.v0.actor.getProfile", {})); 166// }; 167 168// await uploadAudio(); 169// await createTrack(); 170// await createPlaylist(); 171await createLargePlaylist();