atmo.rsvp
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

cleanup vod stuff

Florian cceba427 36c62353

+14 -913
-108
scripts/download-vods.ts
··· 1 - /** 2 - * Downloads all matched ATmosphereConf VOD MP4s using ffmpeg. 3 - * Uses the HLS playlist URL so both audio and video are muxed properly. 4 - * 5 - * Requires: ffmpeg 6 - * 7 - * Usage: npx tsx scripts/download-vods.ts [output-dir] [filter] 8 - * 9 - * Default output dir: ./vods 10 - * Optional filter: case-insensitive substring match on event title 11 - */ 12 - 13 - import { existsSync, mkdirSync } from 'fs'; 14 - import { execFile } from 'child_process'; 15 - import vodMap from '../src/lib/vod-map.json'; 16 - 17 - const STREAM_PLACE_DID = 'did:plc:rbvrr34edl5ddpuwcubjiost'; 18 - const PLAYBACK_BASE = 'https://vod-beta.stream.place/xrpc/place.stream.playback.getVideoPlaylist'; 19 - 20 - const outDir = process.argv[2] || './vods'; 21 - const filter = process.argv[3]?.toLowerCase(); 22 - 23 - function sanitizeFilename(name: string): string { 24 - return name.replace(/[/\\?%*:|"<>]/g, '-').replace(/\s+/g, ' ').trim(); 25 - } 26 - 27 - function getPlaylistUrl(vodRkey: string): string { 28 - const uri = `at://${STREAM_PLACE_DID}/place.stream.video/${vodRkey}`; 29 - return `${PLAYBACK_BASE}?uri=${encodeURIComponent(uri)}`; 30 - } 31 - 32 - function downloadWithFfmpeg(playlistUrl: string, dest: string): Promise<void> { 33 - return new Promise((resolve, reject) => { 34 - const proc = execFile( 35 - 'ffmpeg', 36 - [ 37 - '-y', 38 - '-i', playlistUrl, 39 - '-c', 'copy', 40 - dest 41 - ], 42 - { maxBuffer: 10 * 1024 * 1024 }, 43 - (err) => { 44 - if (err) reject(err); 45 - else resolve(); 46 - } 47 - ); 48 - 49 - // Show ffmpeg progress 50 - proc.stderr?.on('data', (data: Buffer) => { 51 - const line = data.toString(); 52 - const timeMatch = line.match(/time=(\d+:\d+:\d+\.\d+)/); 53 - if (timeMatch) { 54 - process.stdout.write(`\r time: ${timeMatch[1]} `); 55 - } 56 - }); 57 - }); 58 - } 59 - 60 - async function main() { 61 - if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); 62 - 63 - const entries = filter 64 - ? vodMap.filter((e) => e.eventTitle.toLowerCase().includes(filter)) 65 - : vodMap; 66 - 67 - if (entries.length === 0) { 68 - console.log(`No VODs matching "${filter}"`); 69 - return; 70 - } 71 - 72 - console.log(`Downloading ${entries.length} VOD${entries.length > 1 ? 's' : ''} to ${outDir}/\n`); 73 - 74 - let downloaded = 0; 75 - let skipped = 0; 76 - let failed = 0; 77 - 78 - for (const entry of entries) { 79 - const filename = `${sanitizeFilename(entry.eventTitle)}.mp4`; 80 - const dest = `${outDir}/${filename}`; 81 - 82 - if (existsSync(dest)) { 83 - console.log(` SKIP ${filename} (already exists)`); 84 - skipped++; 85 - continue; 86 - } 87 - 88 - console.log(` GET ${filename}`); 89 - 90 - try { 91 - const playlistUrl = getPlaylistUrl(entry.vodRkey); 92 - await downloadWithFfmpeg(playlistUrl, dest); 93 - process.stdout.write('\r done \n'); 94 - downloaded++; 95 - } catch (e) { 96 - process.stdout.write('\n'); 97 - console.log(` FAILED (${e instanceof Error ? e.message : e})`); 98 - failed++; 99 - } 100 - } 101 - 102 - console.log(`\nDone: ${downloaded} downloaded, ${skipped} skipped, ${failed} failed`); 103 - } 104 - 105 - main().catch((e) => { 106 - console.error(e); 107 - process.exit(1); 108 - });
-176
scripts/match-vods.ts
··· 1 - /** 2 - * Matches ATmosphereConf events to stream.place VODs by title. 3 - * Outputs a JSON mapping to src/lib/vod-map.json. 4 - * 5 - * Usage: npx tsx scripts/match-vods.ts 6 - */ 7 - 8 - const STREAM_PLACE_DID = 'did:plc:rbvrr34edl5ddpuwcubjiost'; 9 - const STREAM_PLACE_PDS = 'https://iameli.com'; 10 - const VOD_COLLECTION = 'place.stream.video'; 11 - const CONTRAIL_URL = 'http://contrail.atmo.rsvp'; 12 - 13 - interface VodRecord { 14 - uri: string; 15 - rkey: string; 16 - title: string; 17 - } 18 - 19 - interface EventRecord { 20 - uri: string; 21 - rkey: string; 22 - name: string; 23 - type?: string; 24 - } 25 - 26 - interface VodMapping { 27 - eventRkey: string; 28 - eventTitle: string; 29 - vodRkey: string; 30 - vodTitle: string; 31 - } 32 - 33 - const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); 34 - 35 - function extractRkey(uri: string): string { 36 - return uri.split('/').pop()!; 37 - } 38 - 39 - async function fetchAllVods(): Promise<VodRecord[]> { 40 - const all: VodRecord[] = []; 41 - let cursor: string | undefined; 42 - 43 - do { 44 - const params = new URLSearchParams({ 45 - repo: STREAM_PLACE_DID, 46 - collection: VOD_COLLECTION, 47 - limit: '100' 48 - }); 49 - if (cursor) params.set('cursor', cursor); 50 - 51 - const res = await fetch(`${STREAM_PLACE_PDS}/xrpc/com.atproto.repo.listRecords?${params}`); 52 - if (!res.ok) break; 53 - 54 - const data = (await res.json()) as { 55 - cursor?: string; 56 - records: Array<{ uri: string; value: { title: string } }>; 57 - }; 58 - 59 - for (const r of data.records ?? []) { 60 - all.push({ 61 - uri: r.uri, 62 - rkey: extractRkey(r.uri), 63 - title: r.value.title 64 - }); 65 - } 66 - cursor = data.cursor; 67 - } while (cursor); 68 - 69 - return all; 70 - } 71 - 72 - async function fetchAllEvents(): Promise<EventRecord[]> { 73 - const res = await fetch( 74 - `${CONTRAIL_URL}/xrpc/community.lexicon.calendar.event.listRecords?actor=atmosphereconf.org&sort=startsAt&order=asc&limit=200` 75 - ); 76 - if (!res.ok) throw new Error(`Failed to fetch events: ${res.status}`); 77 - const data = (await res.json()) as { 78 - records: Array<{ 79 - uri: string; 80 - record: { name: string; additionalData?: { isAtmosphereconf?: boolean; type?: string } }; 81 - }>; 82 - }; 83 - 84 - return data.records 85 - .filter((r) => r.record?.additionalData?.isAtmosphereconf) 86 - .map((r) => ({ 87 - uri: r.uri, 88 - rkey: extractRkey(r.uri), 89 - name: r.record.name, 90 - type: r.record.additionalData?.type 91 - })); 92 - } 93 - 94 - function matchEvents(events: EventRecord[], vods: VodRecord[]): VodMapping[] { 95 - const mappings: VodMapping[] = []; 96 - const usedVods = new Set<string>(); 97 - 98 - for (const event of events) { 99 - const eventNorm = normalize(event.name); 100 - 101 - // Exact match 102 - let match = vods.find((v) => !usedVods.has(v.uri) && normalize(v.title) === eventNorm); 103 - 104 - // Substring match 105 - if (!match && eventNorm.length >= 10) { 106 - match = vods.find((v) => { 107 - if (usedVods.has(v.uri)) return false; 108 - const vodNorm = normalize(v.title); 109 - return vodNorm.length >= 10 && (eventNorm.includes(vodNorm) || vodNorm.includes(eventNorm)); 110 - }); 111 - } 112 - 113 - if (match) { 114 - usedVods.add(match.uri); 115 - mappings.push({ 116 - eventRkey: event.rkey, 117 - eventTitle: event.name, 118 - vodRkey: match.rkey, 119 - vodTitle: match.title 120 - }); 121 - } 122 - } 123 - 124 - return mappings; 125 - } 126 - 127 - async function main() { 128 - console.log('Fetching VODs...'); 129 - const vods = await fetchAllVods(); 130 - console.log(`Found ${vods.length} VODs`); 131 - 132 - console.log('Fetching events...'); 133 - const events = await fetchAllEvents(); 134 - console.log(`Found ${events.length} atmosphere conf events`); 135 - 136 - const mappings = matchEvents(events, vods); 137 - console.log(`\nMatched ${mappings.length} of ${events.length} events to VODs`); 138 - 139 - // Show unmatched events 140 - const matchedRkeys = new Set(mappings.map((m) => m.eventRkey)); 141 - const unmatched = events.filter((e) => !matchedRkeys.has(e.rkey)); 142 - if (unmatched.length > 0) { 143 - console.log(`\nUnmatched events (${unmatched.length}):`); 144 - for (const e of unmatched) { 145 - console.log(` [${e.type || '?'}] ${e.name}`); 146 - } 147 - } 148 - 149 - // Show unmatched VODs 150 - const matchedVodRkeys = new Set(mappings.map((m) => m.vodRkey)); 151 - const unmatchedVods = vods.filter((v) => !matchedVodRkeys.has(v.rkey)); 152 - if (unmatchedVods.length > 0) { 153 - console.log(`\nUnmatched VODs (${unmatchedVods.length}):`); 154 - for (const v of unmatchedVods) { 155 - console.log(` - ${v.title}`); 156 - } 157 - } 158 - 159 - // Write the mapping 160 - const outPath = new URL('../src/lib/vod-map.json', import.meta.url); 161 - const output = mappings.map(({ eventRkey, vodRkey, eventTitle, vodTitle }) => ({ 162 - eventRkey, 163 - vodRkey, 164 - eventTitle, 165 - vodTitle 166 - })); 167 - const { writeFileSync } = await import('fs'); 168 - const { fileURLToPath } = await import('url'); 169 - writeFileSync(fileURLToPath(outPath), JSON.stringify(output, null, '\t') + '\n'); 170 - console.log(`\nWrote ${mappings.length} mappings to src/lib/vod-map.json`); 171 - } 172 - 173 - main().catch((e) => { 174 - console.error(e); 175 - process.exit(1); 176 - });
-596
src/lib/vod-map.json
··· 1 - [ 2 - { 3 - "eventRkey": "thurs-lunch", 4 - "vodRkey": "3mi5csvtgdg22", 5 - "eventTitle": "Lunch Break (Not Catered)", 6 - "vodTitle": "Lunch Break" 7 - }, 8 - { 9 - "eventRkey": "Y5XGQQ6", 10 - "vodRkey": "3miabfvrtei26", 11 - "eventTitle": "Abstracting the Appview Workshop", 12 - "vodTitle": "Abstracting the AppView" 13 - }, 14 - { 15 - "eventRkey": "ats26-open", 16 - "vodRkey": "3mi7jorroaa2h", 17 - "eventTitle": "Opening Remarks", 18 - "vodTitle": "Day 2 Opening Remarks!" 19 - }, 20 - { 21 - "eventRkey": "ats26-keynote", 22 - "vodRkey": "3mi2jdevvu626", 23 - "eventTitle": "Keynote: Towards Modular Open Science", 24 - "vodTitle": "Keynote: Towards Modular Open Science with @row1.ca and @matsulab.com" 25 - }, 26 - { 27 - "eventRkey": "ats26-commons", 28 - "vodRkey": "3mi2rtfj5ec2m", 29 - "eventTitle": "Can decentralists cooperate? Rethinking commons and collective action in the age of platforms and AI", 30 - "vodTitle": "Can decentralists cooperate? Rethinking commons and collective action in the age of platforms and AI" 31 - }, 32 - { 33 - "eventRkey": "ats26-biokea", 34 - "vodRkey": "3mi2ryxrnin2a", 35 - "eventTitle": "Reproducible, citation-aware automated paper reviews", 36 - "vodTitle": "Reproducible, citation-aware automated paper reviews @seanjungblluth.bsky.social" 37 - }, 38 - { 39 - "eventRkey": "ats26-skysquare", 40 - "vodRkey": "3mi2u6pl6ah2z", 41 - "eventTitle": "Skysquare is context as a service", 42 - "vodTitle": "Skysquare is context as a service - Travis Simpson - @skysquare.app" 43 - }, 44 - { 45 - "eventRkey": "ats26-viewsift", 46 - "vodRkey": "3mi2son7re62m", 47 - "eventTitle": "Building collective intelligence to reduce division at ViewSift", 48 - "vodTitle": "Building collective intelligence to reduce division at ViewSift @viewsift.com" 49 - }, 50 - { 51 - "eventRkey": "ats26-seams", 52 - "vodRkey": "3mi2tfsbq4p25", 53 - "eventTitle": "Making wisdom together", 54 - "vodTitle": "Making wisdom together - seams.so - @hyl.st" 55 - }, 56 - { 57 - "eventRkey": "ats26-astrosky", 58 - "vodRkey": "3mi2wu5u5rs2i", 59 - "eventTitle": "The Astrosky Ecosystem: An independent online home for astronomy", 60 - "vodTitle": "The Astrosky Ecosystem: An independent online home for astronomy" 61 - }, 62 - { 63 - "eventRkey": "ats26-our-socialmedia-future", 64 - "vodRkey": "3mi33ycoo5d2a", 65 - "eventTitle": "Future of Science Social Media", 66 - "vodTitle": "Future of Science Social Media" 67 - }, 68 - { 69 - "eventRkey": "ats26-research-synthesis", 70 - "vodRkey": "3mi34ps3oym2i", 71 - "eventTitle": "Crowdsourced Research Synthesis on ATProto: Envisioning an Inclusive Future", 72 - "vodTitle": "Crowdsourced Research Synthesis on ATProto: Envisioning an Inclusive Future - Jay Patel - @infotainment.bsky.social" 73 - }, 74 - { 75 - "eventRkey": "ats26-paperskygest", 76 - "vodRkey": "3mi35eplsua23", 77 - "eventTitle": "Studying social media through the Atmosphere", 78 - "vodTitle": "Studying social media through the Atmosphere - " 79 - }, 80 - { 81 - "eventRkey": "ats26-comm-archive", 82 - "vodRkey": "3mi36lcxgce2b", 83 - "eventTitle": "Narrative strands & memetic lineages in community social data using Community Archive", 84 - "vodTitle": "Narrative strands & memetic lineages in community social data using Community Archive - " 85 - }, 86 - { 87 - "eventRkey": "ats26-decentralize", 88 - "vodRkey": "3mi5r2rp7a62e", 89 - "eventTitle": "How (de)centralized is Bluesky, really?", 90 - "vodTitle": "How (de)centralized is Bluesky, really?" 91 - }, 92 - { 93 - "eventRkey": "J9yOpYz", 94 - "vodRkey": "3mi54lyc5ts25", 95 - "eventTitle": "The Aggregation Era burned journalism institutions to the ground. The federated era is emerging from those embers", 96 - "vodTitle": "The Aggregation Era burned journalism institutions to the ground. The federated era is emerging from those embers" 97 - }, 98 - { 99 - "eventRkey": "BzrpDQK", 100 - "vodRkey": "3mi54nec66s2w", 101 - "eventTitle": "Feature / Product / Business: A Framework for Sustainable ATProto Projects", 102 - "vodTitle": "Feature / Product / Business: A Framework for Sustainable ATProto Projects" 103 - }, 104 - { 105 - "eventRkey": "QK9Ae6Y", 106 - "vodRkey": "3mi54oonum62b", 107 - "eventTitle": "Groundings with my Siblings: Lessons Learned Building for Community", 108 - "vodTitle": "Groundings with my Siblings: Lessons Learned Building for Community" 109 - }, 110 - { 111 - "eventRkey": "obLbvQV", 112 - "vodRkey": "3mi56m3hnrq2z", 113 - "eventTitle": "The Economics of Sovereign Media: A Roadmap for AT Protocol", 114 - "vodTitle": "The Economics of Sovereign Media: A Roadmap for AT Protocol" 115 - }, 116 - { 117 - "eventRkey": "obaP26x", 118 - "vodRkey": "3mi56n6j2g22d", 119 - "eventTitle": "Who owns the group chat? Building collaborative spaces on ATProto", 120 - "vodTitle": "Who owns the group chat? Building collaborative spaces on ATProto" 121 - }, 122 - { 123 - "eventRkey": "gDPLaGd", 124 - "vodRkey": "3mi56pnvulh2o", 125 - "eventTitle": "Did Lexicon just accidentally solve the enterprise data problem?", 126 - "vodTitle": "Did Lexicon just accidentally solve the enterprise data problem?" 127 - }, 128 - { 129 - "eventRkey": "ODxNLMM", 130 - "vodRkey": "3mi5a2y3tej2z", 131 - "eventTitle": "This isn't over until we all listen to kpop", 132 - "vodTitle": "This isn't over until we all listen to kpop" 133 - }, 134 - { 135 - "eventRkey": "81VNEBO", 136 - "vodRkey": "3mi5aarspma27", 137 - "eventTitle": "Creators First: Video & Media as the Foundation of a Thriving Creator Economy on ATProto", 138 - "vodTitle": "Creators First: Video & Media as the Foundation of a Thriving Creator Economy on ATProto" 139 - }, 140 - { 141 - "eventRkey": "9qkWqPG", 142 - "vodRkey": "3mi5ag3scgk2k", 143 - "eventTitle": "Building Future of Artificial Intelligence on AT Protocol", 144 - "vodTitle": "Building Future of Artificial Intelligence on AT Protocol" 145 - }, 146 - { 147 - "eventRkey": "VLa69bl", 148 - "vodRkey": "3mi5bya72ub2d", 149 - "eventTitle": "A discussion with news creators", 150 - "vodTitle": "A discussion with news creators" 151 - }, 152 - { 153 - "eventRkey": "7Rrv0E0", 154 - "vodRkey": "3mi5bcpyqg32a", 155 - "eventTitle": "Beyond Bluesky: Community infrastructure", 156 - "vodTitle": "Beyond Bluesky: Community infrastructure" 157 - }, 158 - { 159 - "eventRkey": "VLerG2y", 160 - "vodRkey": "3mi5c65iee42h", 161 - "eventTitle": "Understanding the Landscape of Custom Feeds on Bluesky", 162 - "vodTitle": "Understanding the Landscape of Custom Feeds on Bluesky" 163 - }, 164 - { 165 - "eventRkey": "000Syverson", 166 - "vodRkey": "3mi5hza73vs2z", 167 - "eventTitle": "Sattestations", 168 - "vodTitle": "Sattestations" 169 - }, 170 - { 171 - "eventRkey": "QKlrLXG", 172 - "vodRkey": "3mi5hx4v6cr26", 173 - "eventTitle": "Advocating for Digital Sovereignty: European Experiences and Global Lessons", 174 - "vodTitle": "Advocating for Digital Sovereignty: European Experiences and Global Lessons" 175 - }, 176 - { 177 - "eventRkey": "XxP4RzL", 178 - "vodRkey": "3mi5hy457fu2r", 179 - "eventTitle": "Building Cirrus: a single-user, serverless PDS", 180 - "vodTitle": "Building Cirrus: a single-user, serverless PDS" 181 - }, 182 - { 183 - "eventRkey": "81Xovjr", 184 - "vodRkey": "3mi5itmi65s2z", 185 - "eventTitle": "Feeds Are The New Websites", 186 - "vodTitle": "Feeds Are The New Websites" 187 - }, 188 - { 189 - "eventRkey": "LZxV6dv", 190 - "vodRkey": "3mi5jmsg6kn2m", 191 - "eventTitle": "Consent Before Cryptography", 192 - "vodTitle": "Consent Before Cryptography" 193 - }, 194 - { 195 - "eventRkey": "MelQ8Ak", 196 - "vodRkey": "3mi5k3brfax2b", 197 - "eventTitle": "Rethinking the Client: Why User Choice is the Key to Growth for ATProto", 198 - "vodTitle": "Rethinking the Client: Why User Choice is the Key to Growth for ATProto" 199 - }, 200 - { 201 - "eventRkey": "OD2PpYA", 202 - "vodRkey": "3mi5jrjmk6y2n", 203 - "eventTitle": "Open social tech and geopolitical risk", 204 - "vodTitle": "Open social tech and geopolitical risk" 205 - }, 206 - { 207 - "eventRkey": "ja4ooAa", 208 - "vodRkey": "3mi5lexqbqf2z", 209 - "eventTitle": "Building Public-Interest Infrastructure on ATProto", 210 - "vodTitle": "Building Public-Interest Infrastructure on ATProto" 211 - }, 212 - { 213 - "eventRkey": "gDP6A8N", 214 - "vodRkey": "3mi5lb763vn2j", 215 - "eventTitle": "Account logic in ATProto using Trusted Execution Environments", 216 - "vodTitle": "Account logic in ATProto using Trusted Execution Environments" 217 - }, 218 - { 219 - "eventRkey": "Y561Qv6", 220 - "vodRkey": "3mi5ldvd46r2i", 221 - "eventTitle": "From protocol to product: How Expo powers the next wave of AT Proto applications", 222 - "vodTitle": "From protocol to product: How Expo powers the next wave of AT Proto applications" 223 - }, 224 - { 225 - "eventRkey": "aQ1J9GE", 226 - "vodRkey": "3mi5mvmovsn2i", 227 - "eventTitle": "2026 Atmosphere Report", 228 - "vodTitle": "join us in https://stream.place/stream1.atmosphereconf.org for 2026 Atmosphere Report" 229 - }, 230 - { 231 - "eventRkey": "Bzr448Q", 232 - "vodRkey": "3mi5q3oibtu2i", 233 - "eventTitle": "Why Gander Social?", 234 - "vodTitle": "Why Gander Social?" 235 - }, 236 - { 237 - "eventRkey": "9qP16Kp", 238 - "vodRkey": "3mi5q4ey4232y", 239 - "eventTitle": "Stop Hallucinating the Protocol: Grounding your AI Agents with the Official ATproto Docs", 240 - "vodTitle": "Stop Hallucinating the Protocol: Grounding your AI Agents with the Official ATproto Docs" 241 - }, 242 - { 243 - "eventRkey": "OD6Gd0A", 244 - "vodRkey": "3mi5q57diht27", 245 - "eventTitle": "Semble: Rediscovering the Magic of Trails", 246 - "vodTitle": "Semble: Rediscovering the Magic of Trails" 247 - }, 248 - { 249 - "eventRkey": "000WSocial", 250 - "vodRkey": "3mi5qzkwfe42z", 251 - "eventTitle": "Who, Where, Why, What about W Social", 252 - "vodTitle": "Who, Where, Why, What about W Social" 253 - }, 254 - { 255 - "eventRkey": "QKZoLBX", 256 - "vodRkey": "3mi37ha3edb23", 257 - "eventTitle": "How (de)centralized is Bluesky, really?", 258 - "vodTitle": "How (de)centralized is Bluesky, really? - " 259 - }, 260 - { 261 - "eventRkey": "2EG4YMj", 262 - "vodRkey": "3mi5qywid6z25", 263 - "eventTitle": "What 350,000 users taught me about growing on Open Social", 264 - "vodTitle": "What 350,000 users taught me about growing on Open Social" 265 - }, 266 - { 267 - "eventRkey": "000Ryo", 268 - "vodRkey": "3mi5rl4r4a725", 269 - "eventTitle": "Bridging Social Graphs: How Sky Follower Bridge helps people move to Bluesky", 270 - "vodTitle": "Bridging Social Graphs: How Sky Follower Bridge helps people move to Bluesky" 271 - }, 272 - { 273 - "eventRkey": "Xxd7xqj", 274 - "vodRkey": "3mi5rnhjugb2h", 275 - "eventTitle": "E2EE DMs for Solidarity Social", 276 - "vodTitle": "E2EE DMs for Solidarity Social" 277 - }, 278 - { 279 - "eventRkey": "000Jer", 280 - "vodRkey": "3mi5rm4y7ts2b", 281 - "eventTitle": "The Future of Open Source is Social", 282 - "vodTitle": "The Future of Open Source is Social" 283 - }, 284 - { 285 - "eventRkey": "2EGLPML", 286 - "vodRkey": "3mi5s2x6tkd2d", 287 - "eventTitle": "Burning down data walls in the US Fire Service and Beyond", 288 - "vodTitle": "Burning down data walls in the US Fire Service and Beyond" 289 - }, 290 - { 291 - "eventRkey": "7Rr2zW6", 292 - "vodRkey": "3mi5s5mqmgj2r", 293 - "eventTitle": "Pollen: Prototyping a toolkit for journalists and researchers to restore source transparency in an AI-saturated feed", 294 - "vodTitle": "Pollen: Prototyping a toolkit for journalists and researchers to restore source transparency in an AI-saturated feed" 295 - }, 296 - { 297 - "eventRkey": "lbkWPeN", 298 - "vodRkey": "3mi5s4foj7h2a", 299 - "eventTitle": "Oaklog: Building a community calendar in the Oakland Bay Area", 300 - "vodTitle": "Oaklog: Building a community calendar in the Oakland Bay Area" 301 - }, 302 - { 303 - "eventRkey": "RGqppqd", 304 - "vodRkey": "3mi5stzyxji2e", 305 - "eventTitle": "How Streamplace Works: VODs", 306 - "vodTitle": "How Streamplace Works: VODs" 307 - }, 308 - { 309 - "eventRkey": "EkGROKB", 310 - "vodRkey": "3mi5sn7zmfy23", 311 - "eventTitle": "A Free Press needs Free Protocols", 312 - "vodTitle": "A Free Press needs Free Protocols" 313 - }, 314 - { 315 - "eventRkey": "OD2G9j8", 316 - "vodRkey": "3mi5spj6izy2a", 317 - "eventTitle": "The Phoenix Architecture", 318 - "vodTitle": "The Phoenix Architecture" 319 - }, 320 - { 321 - "eventRkey": "J9EgEdX", 322 - "vodRkey": "3mi5uqr2spg2l", 323 - "eventTitle": "Hypercerts on ATProto: Collective Funding, Evaluation, and Ownership as Social Data", 324 - "vodTitle": "Hypercerts on ATProto: Collective Funding, Evaluation, and Ownership as Social Data" 325 - }, 326 - { 327 - "eventRkey": "PdJ6Q8d", 328 - "vodRkey": "3mi5unzkbat27", 329 - "eventTitle": "Journalism must create its own algorithms", 330 - "vodTitle": "Journalism must create its own algorithms" 331 - }, 332 - { 333 - "eventRkey": "rj8Xv62", 334 - "vodRkey": "3mi5unhqisv22", 335 - "eventTitle": "This Title Left Intentionally Blank", 336 - "vodTitle": "This Title Left Intentionally Blank" 337 - }, 338 - { 339 - "eventRkey": "day-3-closing-remarks", 340 - "vodRkey": "3miafr5amxa2m", 341 - "eventTitle": "Day 3 Closing Remarks", 342 - "vodTitle": "closing remarks" 343 - }, 344 - { 345 - "eventRkey": "VLXBbzJ", 346 - "vodRkey": "3mi7ldfbjwe26", 347 - "eventTitle": "How and Why News Organizations Should Build on the ATProtocol", 348 - "vodTitle": "How and Why News Organizations Should Build on the ATProtocol" 349 - }, 350 - { 351 - "eventRkey": "VLgVWM6", 352 - "vodRkey": "3mi7mb4jbag23", 353 - "eventTitle": "tangled: The Lewis end", 354 - "vodTitle": "tangled: The Lewis end" 355 - }, 356 - { 357 - "eventRkey": "Mej2N5X", 358 - "vodRkey": "3mi7lapdbjc2t", 359 - "eventTitle": "Roomy and community organizing for system change", 360 - "vodTitle": "Roomy and community organizing for system change" 361 - }, 362 - { 363 - "eventRkey": "WOkL11Q", 364 - "vodRkey": "3mi7ok5zmuk2k", 365 - "eventTitle": "How to have more non-english speaking users", 366 - "vodTitle": "How to have more non-english speaking users" 367 - }, 368 - { 369 - "eventRkey": "gDPMMa1", 370 - "vodRkey": "3mi7ofq7ob72a", 371 - "eventTitle": "One Year of Graze - lessons learned funding, building, and growing in the atmosphere", 372 - "vodTitle": "One Year of Graze - lessons learned funding, building, and growing in the atmosphere" 373 - }, 374 - { 375 - "eventRkey": "jaAWVRY", 376 - "vodRkey": "3mi7o5amqqx2d", 377 - "eventTitle": "Bringing Self Sovereign Identities to the Masses via ATproto (and how to maximize coherence between upcoming DID:PLC forks)", 378 - "vodTitle": "Bringing Self Sovereign Identities to the Masses via ATproto" 379 - }, 380 - { 381 - "eventRkey": "ODqQQJA", 382 - "vodRkey": "3mi7pjfdkhv2b", 383 - "eventTitle": "Waiting for the Future to Load", 384 - "vodTitle": "Waiting for the Future to Load" 385 - }, 386 - { 387 - "eventRkey": "1AzdYWM", 388 - "vodRkey": "3mi7pxl6xkz22", 389 - "eventTitle": "Bluenotes: Community Notes for ATProto", 390 - "vodTitle": "Bluenotes: Community Notes for ATProto" 391 - }, 392 - { 393 - "eventRkey": "zxRkxk8", 394 - "vodRkey": "3mi7q5mjlbe2c", 395 - "eventTitle": "Blousques: Case Study on the Challenges in Translating Bluesky's UI", 396 - "vodTitle": "Blousques: Case Study on the Challenges in Translating Bluesky's UI" 397 - }, 398 - { 399 - "eventRkey": "q4qlVLY", 400 - "vodRkey": "3mi7rpxijg725", 401 - "eventTitle": "Community privacy in a decentralized network", 402 - "vodTitle": "Community privacy in a decentralized network" 403 - }, 404 - { 405 - "eventRkey": "kdobWjj", 406 - "vodRkey": "3mi7rvqdpj722", 407 - "eventTitle": "A Fireside Chat on Resonant Computing: Why we wrote the manifesto and where we go from here", 408 - "vodTitle": "A Fireside Chat on Resonant Computing: Why we wrote the manifesto and where we go from here" 409 - }, 410 - { 411 - "eventRkey": "2E9XG1b", 412 - "vodRkey": "3mi7tcodsqu2t", 413 - "eventTitle": "Creating a Safer Web: Blacksky's Moderation Tool", 414 - "vodTitle": "Creating a Safer Web: Blacksky's Moderation Tool" 415 - }, 416 - { 417 - "eventRkey": "9q8ZX5Q", 418 - "vodRkey": "3mi7tqadgv22d", 419 - "eventTitle": "Social Components", 420 - "vodTitle": "Social Components" 421 - }, 422 - { 423 - "eventRkey": "EkexvrN", 424 - "vodRkey": "3mi7tbwgymp25", 425 - "eventTitle": "~~Compete or kill~~ Cooperate and Succeed!", 426 - "vodTitle": "Compete or kill Cooperate and Succeed!" 427 - }, 428 - { 429 - "eventRkey": "GxEe0Vz", 430 - "vodRkey": "3mi7ztge2sf2h", 431 - "eventTitle": "Designing for the social web", 432 - "vodTitle": "Designing for the social web" 433 - }, 434 - { 435 - "eventRkey": "LZ4oWrj", 436 - "vodRkey": "3mia26ffz2j2c", 437 - "eventTitle": "Two Years of Skywatch: Lessons Learned for Community Moderation", 438 - "vodTitle": "Two Years of Skywatch: Lessons Learned for Community Moderation" 439 - }, 440 - { 441 - "eventRkey": "q4QAgVk", 442 - "vodRkey": "3mi7zxhekcu2d", 443 - "eventTitle": "Building decentralized AI on atproto", 444 - "vodTitle": "Building decentralized AI on atproto" 445 - }, 446 - { 447 - "eventRkey": "xXWE9Dv", 448 - "vodRkey": "3mia3vqrg4r23", 449 - "eventTitle": "Data Sovereignty for Games (and Everything Else): Building Decentralized Industry Infrastructure on ATProto", 450 - "vodTitle": "Data Sovereignty for Games (and Everything Else): Building Decentralized Industry Infrastructure on ATProto" 451 - }, 452 - { 453 - "eventRkey": "0Qq9NlZ", 454 - "vodRkey": "3mia3vm5oyd2u", 455 - "eventTitle": "Coop: Open source Trust & Safety infrastructure for all", 456 - "vodTitle": "Coop: Open source Trust & Safety infrastructure for all" 457 - }, 458 - { 459 - "eventRkey": "WObY04Q", 460 - "vodRkey": "3mia3svkvjw22", 461 - "eventTitle": "furryli.st — Building Communities Without Landlords From the Protocol Up", 462 - "vodTitle": "furryli.st — Building Communities Without Landlords From the Protocol Up" 463 - }, 464 - { 465 - "eventRkey": "000WebTiles", 466 - "vodRkey": "3mia5q5vu4q2h", 467 - "eventTitle": "WebTiles Showcase", 468 - "vodTitle": "WebTiles Showcase" 469 - }, 470 - { 471 - "eventRkey": "rjQ96kl", 472 - "vodRkey": "3mia5khdija2h", 473 - "eventTitle": "Protocol Governance & Hard Decentralization", 474 - "vodTitle": "Protocol Governance & Hard Decentralization" 475 - }, 476 - { 477 - "eventRkey": "9qjDJZG", 478 - "vodRkey": "3mia5mbp4tf22", 479 - "eventTitle": "From Toilets to Moths: The Future of Social Media is Weird and Not For Everyone", 480 - "vodTitle": "From Toilets to Moths: The Future of Social Media is Weird and Not For Everyone" 481 - }, 482 - { 483 - "eventRkey": "81gXlXP", 484 - "vodRkey": "3mia74qg3622a", 485 - "eventTitle": "Building Bridgy, Not Walls", 486 - "vodTitle": "Building Bridgy, Not Walls" 487 - }, 488 - { 489 - "eventRkey": "5B02jaM", 490 - "vodRkey": "3mia7iun75l2x", 491 - "eventTitle": "Keywords vs Embeddings", 492 - "vodTitle": "Keywords vs Embeddings" 493 - }, 494 - { 495 - "eventRkey": "q4QdXj7", 496 - "vodRkey": "3mia7dt6fnp2m", 497 - "eventTitle": "ATProto design philosophy behind BookHive", 498 - "vodTitle": "ATProto design philosophy behind BookHive" 499 - }, 500 - { 501 - "eventRkey": "QKNkKMX", 502 - "vodRkey": "3miabh2g67c2c", 503 - "eventTitle": "Scaling the Atmosphere", 504 - "vodTitle": "Scaling the Atmosphere" 505 - }, 506 - { 507 - "eventRkey": "aQJAWl9", 508 - "vodRkey": "3miac22soys22", 509 - "eventTitle": "Affordances of the Atmosphere", 510 - "vodTitle": "Affordances of the Atmosphere" 511 - }, 512 - { 513 - "eventRkey": "ob8N65V", 514 - "vodRkey": "3miacmdq4sm2a", 515 - "eventTitle": "How to use Bluesky to easily and securely preview a software product to users.", 516 - "vodTitle": "How to use Bluesky to easily and securely preview a software product to users." 517 - }, 518 - { 519 - "eventRkey": "xX5yRJr", 520 - "vodRkey": "3miac5ez6a22r", 521 - "eventTitle": "Skylimit: A curating web client with fine-grained controls to mimic the newspaper experience", 522 - "vodTitle": "Skylimit: A curating web client with fine-grained controls to mimic the newspaper experience" 523 - }, 524 - { 525 - "eventRkey": "686gZde", 526 - "vodRkey": "3miac3urhgt23", 527 - "eventTitle": "Using GraphQL to build with ATProto", 528 - "vodTitle": "Using GraphQL to build with ATProto" 529 - }, 530 - { 531 - "eventRkey": "ZjL74D0", 532 - "vodRkey": "3miacmwnd2z2z", 533 - "eventTitle": "Jacquard Magic: how to make atproto actually easy with Rust", 534 - "vodTitle": "Jacquard Magic: how to make atproto actually easy with Rust" 535 - }, 536 - { 537 - "eventRkey": "RG6Nepp", 538 - "vodRkey": "3miadb23fme2a", 539 - "eventTitle": "Wherever You Get Your Podcasts: Interoperability in the Atmosphere", 540 - "vodTitle": "Wherever You Get Your Podcasts: Interoperability in the Atmosphere" 541 - }, 542 - { 543 - "eventRkey": "000TLog", 544 - "vodRkey": "3miacona6fc2e", 545 - "eventTitle": "AT Transparency Logs: accountable record collections", 546 - "vodTitle": "AT Transparency Logs: accountable record collections" 547 - }, 548 - { 549 - "eventRkey": "ZjMOl7o", 550 - "vodRkey": "3miadd43al32c", 551 - "eventTitle": "Matadata! Publishing scientific data straight to AT", 552 - "vodTitle": "Matadata! Publishing scientific data straight to AT" 553 - }, 554 - { 555 - "eventRkey": "A7YLlpl", 556 - "vodRkey": "3miaec7exqi25", 557 - "eventTitle": "An artist dreaming in the Atmosphere: visions about community, sustainability and creativity", 558 - "vodTitle": "An artist dreaming in the Atmosphere: visions about community, sustainability and creativity" 559 - }, 560 - { 561 - "eventRkey": "PdNOkAP", 562 - "vodRkey": "3miaef2b4mh2h", 563 - "eventTitle": "Rewilding the internet with ATProto", 564 - "vodTitle": "Rewilding the internet with ATProto" 565 - }, 566 - { 567 - "eventRkey": "QKqWDrG", 568 - "vodRkey": "3miadvkvhfv2h", 569 - "eventTitle": "DID:PLC War Games", 570 - "vodTitle": "DID:PLC War Games" 571 - }, 572 - { 573 - "eventRkey": "gDELD0M", 574 - "vodRkey": "3mi54jqrm372z", 575 - "eventTitle": "Landslide", 576 - "vodTitle": "(#KelpFacts) Landslide" 577 - }, 578 - { 579 - "eventRkey": "MeWBzWX", 580 - "vodRkey": "3mi7kaogqjs22", 581 - "eventTitle": "npmx: a modern browser for the npm registry", 582 - "vodTitle": "npmx - a fast, modern browser for the npm registry" 583 - }, 584 - { 585 - "eventRkey": "day-4-closing-remarks", 586 - "vodRkey": "3mi5wqocga62h", 587 - "eventTitle": "Day 4 Closing Remarks", 588 - "vodTitle": "Closing remarks, I guess, if i have to." 589 - }, 590 - { 591 - "eventRkey": "3mhzjk45462rg", 592 - "vodRkey": "3mi2ikg6gij26", 593 - "eventTitle": "ATScience Unconference", 594 - "vodTitle": "ATScience at ATmosphereConf 2026!" 595 - } 596 - ]
+6 -25
src/lib/vods.ts
··· 1 - import vodMap from './vod-map.json'; 2 - 3 - const STREAM_PLACE_DID = 'did:plc:rbvrr34edl5ddpuwcubjiost'; 4 1 const VOD_PLAYBACK_BASE = 'https://vod-beta.stream.place/xrpc/place.stream.playback.getVideoPlaylist'; 5 2 6 3 export interface VodRecord { 7 - vodRkey: string; 8 - vodTitle: string; 4 + atUri: string; 9 5 playlistUrl: string; 10 6 } 11 7 12 - function makePlaylistUrl(vodRkey: string): string { 13 - const uri = `at://${STREAM_PLACE_DID}/place.stream.video/${vodRkey}`; 14 - return `${VOD_PLAYBACK_BASE}?uri=${encodeURIComponent(uri)}`; 15 - } 16 - 17 - const vodByEventRkey = new Map<string, VodRecord>(); 18 - for (const entry of vodMap) { 19 - vodByEventRkey.set(entry.eventRkey, { 20 - vodRkey: entry.vodRkey, 21 - vodTitle: entry.vodTitle, 22 - playlistUrl: makePlaylistUrl(entry.vodRkey) 23 - }); 24 - } 25 - 26 - export function getVodForEvent(eventRkey: string): VodRecord | null { 27 - return vodByEventRkey.get(eventRkey) ?? null; 28 - } 29 - 30 - export function getAllEventVods(): Map<string, VodRecord> { 31 - return vodByEventRkey; 8 + export function vodFromAtUri(atUri: string): VodRecord { 9 + return { 10 + atUri, 11 + playlistUrl: `${VOD_PLAYBACK_BASE}?uri=${encodeURIComponent(atUri)}` 12 + }; 32 13 }
+3 -2
src/routes/(app)/p/[actor]/e/[rkey]/+page.server.ts
··· 12 12 listEventAttendeesFromContrail, 13 13 RSVP_HYDRATE_LIMIT 14 14 } from '$lib/contrail'; 15 - import { getVodForEvent } from '$lib/vods'; 15 + import { vodFromAtUri } from '$lib/vods'; 16 16 17 17 export async function load({ params, locals, url }) { 18 18 const { rkey } = params; ··· 44 44 | Array<{ id: string; name: string }> 45 45 | undefined) ?? []; 46 46 47 - const vod = isAtmosphereconf ? getVodForEvent(rkey) : null; 47 + const vodAtUri = (eventData.additionalData as Record<string, unknown> | undefined)?.vodAtUri as string | undefined; 48 + const vod = vodAtUri ? vodFromAtUri(vodAtUri) : null; 48 49 49 50 const [attendees, viewerRsvpRecord, parentEvent, ...speakerProfiles] = await Promise.all([ 50 51 listEventAttendeesFromContrail(fullEventRecord.uri),
+1 -1
src/routes/(app)/p/[actor]/e/[rkey]/+page.svelte
··· 376 376 <!-- VOD --> 377 377 {#if data.vod} 378 378 <div class="mb-6"> 379 - <VodPlayer playlistUrl={data.vod.playlistUrl} title={data.vod.vodTitle} /> 379 + <VodPlayer playlistUrl={data.vod.playlistUrl} title={eventData.name} /> 380 380 </div> 381 381 {/if} 382 382
+4 -5
src/routes/(app)/p/atmosphereconf.org/+page.server.ts
··· 4 4 listEventRecordsFromContrail, 5 5 contrail 6 6 } from '$lib/contrail'; 7 - import { getAllEventVods, type VodRecord } from '$lib/vods'; 7 + import { vodFromAtUri, type VodRecord } from '$lib/vods'; 8 8 9 9 export async function load({ locals }) { 10 10 const actor = 'atmosphereconf.org'; ··· 41 41 } 42 42 } 43 43 44 - // Build event URI → VOD map from static mapping 45 - const vodsByRkey = getAllEventVods(); 44 + // Build event URI → VOD map from additionalData.vodAtUri 46 45 const eventVods: Record<string, VodRecord> = {}; 47 46 for (const event of events) { 48 - const vod = vodsByRkey.get(event.rkey); 49 - if (vod) eventVods[event.uri] = vod; 47 + const vodAtUri = (event.additionalData as Record<string, unknown> | undefined)?.vodAtUri as string | undefined; 48 + if (vodAtUri) eventVods[event.uri] = vodFromAtUri(vodAtUri); 50 49 } 51 50 52 51 return {