data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

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

fetch last track from teal.fm

dawn cb9e8adb c73e6c3d

+74 -29
+2
src/lib/activity.ts
··· 33 33 }; 34 34 35 35 const fetchTangledActivity = async (): Promise<Activity[]> => { 36 + // todo: auto resolve pds and knots 36 37 const did = 'did:plc:dfl62fgb7wtjj3fcbb72naae'; 37 38 const pds = 'https://zwsp.xyz'; 38 39 const knot = 'https://knot.gaze.systems'; 39 40 const activities: Activity[] = []; 40 41 41 42 try { 43 + // todo: fetch until we exhaust 42 44 const listRes = await fetch( 43 45 `${pds}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=sh.tangled.repo` 44 46 );
+69 -26
src/lib/lastfm.ts
··· 1 1 import { env } from '$env/dynamic/private'; 2 2 import { get, writable } from 'svelte/store'; 3 3 4 - const GET_RECENT_TRACKS_ENDPOINT = 'https://api.listenbrainz.org/1/user/90008/listens?count=1'; 4 + const DID = 'did:plc:dfl62fgb7wtjj3fcbb72naae'; 5 + const PDS = 'https://zwsp.xyz'; 5 6 const LAST_TRACK_FILE = `${env.WEBSITE_DATA_DIR}/last_track.json`; 6 7 7 8 type LastTrack = { 8 9 name: string; 9 10 artist: string; 10 11 image: string | null; 11 - link: string; 12 + link: string | null; 12 13 when: number; 14 + status: 'playing' | 'played'; 13 15 }; 14 16 const lastTrack = writable<LastTrack | null>(null); 15 17 ··· 23 25 } 24 26 }; 25 27 26 - const getTrackCoverArt = (track: any) => { 27 - // parse origin url to see if it matches youtube.com / music.youtube.com and extract video id 28 - const originUrl = track.additional_info?.origin_url ?? null; 29 - if (originUrl && (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com'))) { 30 - const videoId = new URL(originUrl).searchParams.get('v'); 31 - if (!videoId) return null; 32 - return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; 28 + const getTrackCoverArt = (originUrl: string | null | undefined) => { 29 + if (!originUrl) return null; 30 + let videoId: string | null = null; 31 + 32 + try { 33 + if (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com')) { 34 + videoId = new URL(originUrl).searchParams.get('v'); 35 + } else if (originUrl.includes('youtu.be')) { 36 + videoId = originUrl.split('youtu.be/')[1]?.split('?')[0]; 37 + } 38 + } catch { 39 + return null; 33 40 } 34 - return null; 41 + 42 + if (!videoId) return null; 43 + return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`; 35 44 }; 36 45 37 46 const joinArtists = (artists: any[]) => { 38 - if (artists.length === 0) return null; 39 - let result = ''; 40 - for (const artist of artists) { 41 - result += artist.artist_credit_name + artist.join_phrase; 42 - } 43 - return result; 47 + if (!artists || artists.length === 0) return null; 48 + return artists.map((a) => a.artistName).join(', '); 44 49 }; 45 50 46 51 export const updateNowPlayingTrack = async () => { 47 52 try { 48 - const resp = await (await fetch(GET_RECENT_TRACKS_ENDPOINT)).json(); 49 - const track = resp.payload.listens[0]?.track_metadata; 50 - const mapping = track.mbid_mapping ?? {}; 53 + let track: any = null; 54 + let when: number = Date.now(); 55 + let status: 'playing' | 'played' = 'played'; 56 + 57 + try { 58 + const statusRes = await fetch( 59 + `${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=fm.teal.alpha.actor.status&rkey=self` 60 + ); 61 + if (statusRes.ok) { 62 + const statusData = await statusRes.json(); 63 + if (statusData.value?.item) { 64 + track = statusData.value.item; 65 + if (track.playedTime) when = new Date(track.playedTime).getTime(); 66 + status = 'playing'; 67 + } 68 + } 69 + } catch (err) { 70 + console.log('could not fetch teal status:', err); 71 + } 72 + 73 + if (!track) { 74 + try { 75 + const playRes = await fetch( 76 + `${PDS}/xrpc/com.atproto.repo.listRecords?repo=${DID}&collection=fm.teal.alpha.feed.play&limit=1` 77 + ); 78 + if (playRes.ok) { 79 + const playData = await playRes.json(); 80 + if (playData.records.length > 0) { 81 + track = playData.records[0].value; 82 + if (track.playedTime) when = new Date(track.playedTime).getTime(); 83 + status = 'played'; 84 + } 85 + } 86 + } catch (err) { 87 + console.log('could not fetch teal history:', err); 88 + } 89 + } 90 + 51 91 if (!track) return; 52 - const data = { 53 - name: mapping.recording_name ?? track.track_name, 54 - artist: joinArtists(mapping.artists ?? []) ?? track.artist_name, 55 - image: getTrackCoverArt(track), 56 - link: track.additional_info?.origin_url ?? null, 57 - when: resp.payload.latest_listen_ts ? resp.payload.latest_listen_ts * 1000 : Date.now() 92 + 93 + const data: LastTrack = { 94 + name: track.trackName, 95 + artist: joinArtists(track.artists) ?? 'Unknown Artist', 96 + image: getTrackCoverArt(track.originUrl), 97 + link: track.originUrl ?? null, 98 + when: when, 99 + status: status 58 100 }; 101 + 59 102 lastTrack.set(data); 60 103 await Deno.writeTextFile(LAST_TRACK_FILE, JSON.stringify(data)); 61 104 } catch (why) { 62 - console.log('could not fetch last fm: ', why); 105 + console.log('could not fetch teal fm: ', why); 63 106 } 64 107 }; 65 108
+2 -2
src/routes/(site)/+page.svelte
··· 172 172 <p 173 173 class="text-shadow-green text-ralsei-green-light text-sm text-ellipsis text-nowrap overflow-hidden max-w-[50ch]" 174 174 > 175 - <span class="text-sm text-shadow-white text-ralsei-white">listened to</span> 175 + <span class="text-sm text-shadow-white text-ralsei-white">{data.lastTrack.status === 'playing' ? 'listening to' : 'listened to'}</span> 176 176 <a 177 177 title={data.lastTrack.name} 178 - href={data.lastTrack.link ?? 'https://listenbrainz.org/user/90008/'} 178 + href={data.lastTrack.link ?? 'https://tealfm-slice.wisp.place/profile/ptr.pet/scrobbles'} 179 179 class="hover:underline motion-safe:hover:animate-squiggle">{data.lastTrack.name}</a 180 180 > 181 181 </p>
+1 -1
src/routes/(site)/about/+page.md
··· 52 52 53 53 #### /other/ 54 54 55 - this thing likes to consume audio data, mainly of the music form. you can check its [youtube music profile](https://music.youtube.com/channel/UCE_r0yMNQhOWituywmOJgzA?si=7DTUV9PFqcKxJyl1) and its [listenbrainz profile](https://listenbrainz.org/user/90008/) to see some of what it consumes usually. 55 + this thing likes to consume audio data, mainly of the music form. you can check its [youtube music profile](https://music.youtube.com/channel/UCE_r0yMNQhOWituywmOJgzA?si=7DTUV9PFqcKxJyl1) and its [teal.fm profile](https://tealfm-slice.wisp.place/profile/ptr.pet/scrobbles) to see some of what it consumes usually. 56 56 57 57 #### /appendix/ 58 58