A Chrome extension that scrobbles NTS Radio tracks to teal.fm
4
fork

Configure Feed

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

update submission client name

+58 -45
+58 -45
atproto.js
··· 3 3 class ATProtoClient { 4 4 constructor() { 5 5 this.session = null; 6 - this.pds = 'https://bsky.social'; 6 + this.pds = "https://bsky.social"; 7 7 } 8 8 9 9 async login(identifier, password, pdsUrl) { ··· 14 14 await chrome.storage.local.set({ pdsUrl }); 15 15 } 16 16 17 - const response = await fetch(`${this.pds}/xrpc/com.atproto.server.createSession`, { 18 - method: 'POST', 19 - headers: { 20 - 'Content-Type': 'application/json', 21 - }, 22 - body: JSON.stringify({ 23 - identifier, 24 - password, 25 - }), 26 - }); 17 + const response = await fetch( 18 + `${this.pds}/xrpc/com.atproto.server.createSession`, 19 + { 20 + method: "POST", 21 + headers: { 22 + "Content-Type": "application/json", 23 + }, 24 + body: JSON.stringify({ 25 + identifier, 26 + password, 27 + }), 28 + } 29 + ); 27 30 28 31 if (!response.ok) { 29 32 throw new Error(`Login failed: ${response.statusText}`); ··· 36 39 37 40 return this.session; 38 41 } catch (error) { 39 - console.error('AT Proto login error:', error); 42 + console.error("AT Proto login error:", error); 40 43 throw error; 41 44 } 42 45 } 43 46 44 47 async loadSession() { 45 - const data = await chrome.storage.local.get(['atprotoSession', 'pdsUrl']); 48 + const data = await chrome.storage.local.get(["atprotoSession", "pdsUrl"]); 46 49 if (data.pdsUrl) { 47 50 this.pds = data.pdsUrl; 48 51 } ··· 55 58 56 59 async logout() { 57 60 this.session = null; 58 - await chrome.storage.local.remove('atprotoSession'); 61 + await chrome.storage.local.remove("atprotoSession"); 59 62 } 60 63 61 64 async createScrobbleRecord(trackInfo) { 62 65 if (!this.session) { 63 - throw new Error('Not authenticated'); 66 + throw new Error("Not authenticated"); 64 67 } 65 68 66 69 // Build the teal.fm play record 67 70 const record = { 68 - $type: 'fm.teal.alpha.feed.play', 71 + $type: "fm.teal.alpha.feed.play", 69 72 trackName: trackInfo.track, 70 73 playedTime: new Date(trackInfo.timestamp).toISOString(), 71 - submissionClientAgent: 'fm.teal.nts-scrobbler/1.0.0', 72 - musicServiceBaseDomain: 'nts.live' 74 + submissionClientAgent: "nts-teal-piper/1.0.0", 75 + musicServiceBaseDomain: "nts.live", 73 76 }; 74 77 75 78 // Add MusicBrainz metadata if available ··· 96 99 97 100 // Fallback: if no MusicBrainz artists, use basic artist info 98 101 if (!record.artists && trackInfo.artist) { 99 - record.artists = [{ 100 - artistName: trackInfo.artist 101 - }]; 102 + record.artists = [ 103 + { 104 + artistName: trackInfo.artist, 105 + }, 106 + ]; 102 107 } 103 108 104 109 // Add optional fields ··· 107 112 } 108 113 109 114 try { 110 - const response = await fetch(`${this.pds}/xrpc/com.atproto.repo.createRecord`, { 111 - method: 'POST', 112 - headers: { 113 - 'Content-Type': 'application/json', 114 - 'Authorization': `Bearer ${this.session.accessJwt}`, 115 - }, 116 - body: JSON.stringify({ 117 - repo: this.session.did, 118 - collection: 'fm.teal.alpha.feed.play', 119 - record, 120 - }), 121 - }); 115 + const response = await fetch( 116 + `${this.pds}/xrpc/com.atproto.repo.createRecord`, 117 + { 118 + method: "POST", 119 + headers: { 120 + "Content-Type": "application/json", 121 + Authorization: `Bearer ${this.session.accessJwt}`, 122 + }, 123 + body: JSON.stringify({ 124 + repo: this.session.did, 125 + collection: "fm.teal.alpha.feed.play", 126 + record, 127 + }), 128 + } 129 + ); 122 130 123 131 if (!response.ok) { 124 132 const errorText = await response.text(); 125 - throw new Error(`Failed to create record: ${response.statusText} - ${errorText}`); 133 + throw new Error( 134 + `Failed to create record: ${response.statusText} - ${errorText}` 135 + ); 126 136 } 127 137 128 138 return await response.json(); 129 139 } catch (error) { 130 - console.error('Error creating scrobble record:', error); 140 + console.error("Error creating scrobble record:", error); 131 141 throw error; 132 142 } 133 143 } 134 144 135 145 async refreshSession() { 136 146 if (!this.session?.refreshJwt) { 137 - throw new Error('No refresh token available'); 147 + throw new Error("No refresh token available"); 138 148 } 139 149 140 150 try { 141 - const response = await fetch(`${this.pds}/xrpc/com.atproto.server.refreshSession`, { 142 - method: 'POST', 143 - headers: { 144 - 'Authorization': `Bearer ${this.session.refreshJwt}`, 145 - }, 146 - }); 151 + const response = await fetch( 152 + `${this.pds}/xrpc/com.atproto.server.refreshSession`, 153 + { 154 + method: "POST", 155 + headers: { 156 + Authorization: `Bearer ${this.session.refreshJwt}`, 157 + }, 158 + } 159 + ); 147 160 148 161 if (!response.ok) { 149 - throw new Error('Session refresh failed'); 162 + throw new Error("Session refresh failed"); 150 163 } 151 164 152 165 this.session = await response.json(); ··· 154 167 155 168 return this.session; 156 169 } catch (error) { 157 - console.error('Session refresh error:', error); 170 + console.error("Session refresh error:", error); 158 171 throw error; 159 172 } 160 173 } ··· 165 178 } 166 179 167 180 // Make available to other scripts 168 - if (typeof module !== 'undefined' && module.exports) { 181 + if (typeof module !== "undefined" && module.exports) { 169 182 module.exports = ATProtoClient; 170 183 }