Fix now-playing scraper, quit hang, tray icon, and tray race (v1.0.1)

- Read the track from the Media Session API, falling back to the legacy
  ytmusic-player-bar scrape. Current YT Music layouts render no player bar,
  so the API and tray stayed empty while music played. Artist is also clean
  now (no "• views • likes" suffix).
- Terminate connected WebSocket clients before closing the server. ws's
  close() waits for every client to disconnect, so quitting with an overlay
  open hung forever, leaving the window playing and the port bound.
- Replace the tray icon with a valid PNG (the old one had a bad IDAT CRC and
  decoded to an empty image) and test that its chunks are well formed.
- Create the tray before the window so early state updates are not dropped.
- Bump version to 1.0.1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 11:37:38 -07:00
co-authored by Claude Sonnet 5.5
parent f73ea93dde
commit e49078ee03
11 changed files with 192 additions and 54 deletions
+63
View File
@@ -0,0 +1,63 @@
// Extracts the current track from a YouTube Music page. `doc` and `nav` are
// injected so this stays testable without a browser.
function largestArtwork(artwork) {
if (!artwork || !artwork.length) return null;
const area = (a) => {
const [w, h] = String(a.sizes || '').split('x').map(Number);
return (w || 0) * (h || 0);
};
return artwork.reduce((best, a) => (area(a) > area(best) ? a : best)).src || null;
}
// Preferred source: YT Music publishes the track via the Media Session API
// even when its player-bar markup changes.
function fromMediaSession(nav) {
const session = nav && nav.mediaSession;
const meta = session && session.metadata;
if (!meta || !meta.title) return null;
return {
title: meta.title,
artist: meta.artist || null,
albumArt: largestArtwork(meta.artwork),
isPlaying: session.playbackState === 'playing',
};
}
// Fallback for older YT Music layouts that still render ytmusic-player-bar.
function fromPlayerBar(doc) {
const titleEl =
doc.querySelector('ytmusic-player-bar .title') ||
doc.querySelector('.ytmusic-player-bar .title');
const title = titleEl ? titleEl.textContent.trim() : null;
if (!title) return null;
const bylineEl =
doc.querySelector('ytmusic-player-bar .byline') ||
doc.querySelector('.ytmusic-player-bar .byline');
const artImg =
doc.querySelector('ytmusic-player-bar .middle-controls .thumbnail-image-wrapper img') ||
doc.querySelector('ytmusic-player-bar .image img') ||
doc.querySelector('ytmusic-player-bar img.image');
// The button's title is the action it will perform: "Pause" while playing.
const btn = doc.querySelector('#play-pause-button');
let isPlaying = false;
if (btn) {
const label = (btn.getAttribute('title') || btn.getAttribute('aria-label') || '').toLowerCase();
isPlaying = label.includes('pause');
}
return {
title,
artist: bylineEl ? bylineEl.textContent.trim() : null,
albumArt: artImg ? artImg.src : null,
isPlaying,
};
}
function scrapeNowPlaying(doc, nav) {
return fromMediaSession(nav) || fromPlayerBar(doc);
}
module.exports = { scrapeNowPlaying };