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

fix login copy, show last 3 scrobbles in the popup

+89 -6
+12
background.js
··· 42 42 sendResponse({ authenticated: atproto.isAuthenticated() }); 43 43 } else if (message.type === "GET_CURRENT_TRACK") { 44 44 sendResponse({ track: currentTrack }); 45 + } else if (message.type === "GET_RECENT_TRACKS") { 46 + sendResponse({ tracks: recentTracks }); 45 47 } 46 48 }); 47 49 ··· 92 94 // Add to recent tracks 93 95 addToRecentTracks(trackInfo); 94 96 97 + // Notify popup of scrobble success 98 + chrome.runtime.sendMessage({ type: 'SCROBBLE_SUCCESS' }).catch(() => { 99 + // Ignore errors if popup is not open 100 + }); 101 + 95 102 // Show notification 96 103 chrome.notifications.create({ 97 104 type: "basic", ··· 113 120 114 121 // Add to recent tracks 115 122 addToRecentTracks(trackInfo); 123 + 124 + // Notify popup of scrobble success 125 + chrome.runtime.sendMessage({ type: 'SCROBBLE_SUCCESS' }).catch(() => { 126 + // Ignore errors if popup is not open 127 + }); 116 128 117 129 chrome.notifications.create({ 118 130 type: "basic",
+15 -5
popup.html
··· 1 1 <!DOCTYPE html> 2 2 <html> 3 + 3 4 <head> 4 5 <meta charset="UTF-8"> 5 6 <title>NTS Radio Scrobbler</title> 6 7 <link rel="stylesheet" href="styles.css"> 7 8 </head> 9 + 8 10 <body> 9 11 <div class="container"> 10 12 <h1>NTS Radio Scrobbler</h1> 11 13 12 14 <div id="auth-section"> 13 15 <div id="login-form"> 14 - <h2>Login to Bluesky</h2> 16 + <h2>Login to the AT Protocol</h2> 15 17 <input type="text" id="pds-url" placeholder="PDS URL (default: https://bsky.social)" /> 16 18 <input type="text" id="identifier" placeholder="Handle or email" /> 17 - <input type="password" id="password" placeholder="Password" /> 19 + <input type="password" id="password" placeholder="App Password" /> 18 20 <button id="login-btn">Login</button> 19 21 <div id="error-msg" class="error"></div> 20 22 </div> 21 23 22 24 <div id="logged-in" style="display: none;"> 23 - <h2>Connected to Bluesky</h2> 25 + <h2>Connected to the AT Protocol</h2> 24 26 <p id="user-info"></p> 25 27 <button id="logout-btn">Logout</button> 26 28 </div> ··· 31 33 <div id="current-track"> 32 34 <p class="info">No track detected</p> 33 35 </div> 36 + <div id="recent-scrobbles"> 37 + <h3>Recent Scrobbles</h3> 38 + <div id="recent-tracks-list"> 39 + <p class="info small">No recent scrobbles</p> 40 + </div> 41 + </div> 34 42 </div> 35 43 36 44 <div id="settings-section"> ··· 42 50 </div> 43 51 44 52 <div id="status-section"> 45 - <p class="info">Visit <a href="https://www.nts.live" target="_blank">nts.live</a> and start listening to scrobble tracks!</p> 53 + <p class="info">Visit <a href="https://www.nts.live" target="_blank">nts.live</a> and start listening to scrobble 54 + tracks!</p> 46 55 </div> 47 56 </div> 48 57 49 58 <script src="popup.js"></script> 50 59 </body> 51 - </html> 60 + 61 + </html>
+22 -1
popup.js
··· 29 29 pdsUrlInput.value = settings.pdsUrl; 30 30 } 31 31 32 - // Get current track 32 + // Get current track and recent scrobbles 33 33 updateCurrentTrack(); 34 + updateRecentScrobbles(); 34 35 35 36 // Listen for track updates 36 37 chrome.runtime.onMessage.addListener((message) => { 37 38 if (message.type === 'TRACK_UPDATE') { 38 39 updateCurrentTrack(); 40 + } else if (message.type === 'SCROBBLE_SUCCESS') { 41 + updateRecentScrobbles(); 39 42 } 40 43 }); 41 44 ··· 112 115 `; 113 116 } else { 114 117 currentTrackDiv.innerHTML = '<p class="info">No track detected</p>'; 118 + } 119 + } 120 + 121 + async function updateRecentScrobbles() { 122 + const response = await chrome.runtime.sendMessage({ type: 'GET_RECENT_TRACKS' }); 123 + const recentTracksList = document.getElementById('recent-tracks-list'); 124 + 125 + if (response && response.tracks && response.tracks.length > 0) { 126 + // Show only the latest 3 scrobbles 127 + const latestTracks = response.tracks.slice(0, 3); 128 + recentTracksList.innerHTML = latestTracks.map(track => ` 129 + <div class="recent-track"> 130 + <span class="recent-track-title">${track.track}</span> 131 + <span class="recent-track-artist">${track.artist}</span> 132 + </div> 133 + `).join(''); 134 + } else { 135 + recentTracksList.innerHTML = '<p class="info small">No recent scrobbles</p>'; 115 136 } 116 137 } 117 138 });
+40
styles.css
··· 134 134 font-size: 12px; 135 135 color: #666; 136 136 } 137 + 138 + #recent-scrobbles { 139 + margin-top: 16px; 140 + } 141 + 142 + #recent-scrobbles h3 { 143 + font-size: 12px; 144 + font-weight: 600; 145 + color: #999; 146 + margin: 0 0 8px 0; 147 + text-transform: uppercase; 148 + letter-spacing: 0.5px; 149 + } 150 + 151 + #recent-tracks-list { 152 + display: flex; 153 + flex-direction: column; 154 + gap: 6px; 155 + } 156 + 157 + .recent-track { 158 + display: flex; 159 + flex-direction: column; 160 + font-size: 11px; 161 + line-height: 1.4; 162 + color: #999; 163 + } 164 + 165 + .recent-track-title { 166 + color: #666; 167 + } 168 + 169 + .recent-track-artist { 170 + color: #999; 171 + } 172 + 173 + .info.small { 174 + font-size: 11px; 175 + color: #999; 176 + }