Somewhere, I'm online and listening to music

cdrnsf1 pts0 comments

Somewhere, I'm online and listening to music • Cory DransfeldtSkip to main content Featured July 14, 2026Somewhere, I'm online and listening to music<br>developmentindie webwebdevtechmusicnavidrome<br>/posts/2026/somewhere-im-online-and-listening-to-musicFor a while now, my home page has shown whatever song I'm listening to (or, if idle, the last one I played). Initially, it was powered by last.fm. As I've built my own scrobbler, I populated the element from my own data. The static implementation is straightforward: I store each listen as a record. When a listen is inserted, the cache is cleared and warmed, and an update is displayed.What I've been kicking around as an idea has been to have a live progress display to complement this. I implemented something like this briefly while using Jellyfin to listen to music, but Jellyfin's webhooks weren't made for a live update like this.I've been using a Navidrome plugin to scrobble to my own API and this was a good opportunity to try and expand on it. Luckily, Navidrome added a playback_report method in v0.62 with exactly the data I needed: state, position_ms, playback_rate, and the track duration. When I'd attempted this with Jellyfin, I'd done so by advancing every single tick using a webhook payload. In this case, I'm pushing an anchor — position, playback rate, duration — and the bar's motion is a CSS animation with a negative delay, so the browser interpolates. A new fragment only lands when the live state diverges from the predicted state: pause, seek or track change. A paused track renders differently, but it's the same anchor with the rate frozen.The other challenge here is that I wanted this to work without JavaScript. JavaScript can enhance the implementation, but it should be enhanced from a state where progress is already updated live on the page. This is also what I first shipped: the now playing display as its own document, loaded in an iframe whose connection stays open.The iframe works and it works without JavaScript, but there's a cost: if the response never completes, the page never stops loading. Everything works. It's cosmetic. JavaScript lets us paper over that as an enhancement.I'd tried setting loading="lazy" on the iframe, but that only exempts the frame while it's out of view — this is above the fold.The enhancement here took the form of server-sent events. The same fragments the iframe gets — byte for byte, same markup, same CSS — but sent to an EventSource. A background fetch written into a mount point on the page. No loading spinner and the JavaScript-free implementation acts as the noscript fallback.Six failures<br>Caddy's encode zstd gzip compressed the now playing event stream, and compressing a never-ending response buffers it. Setting flush_interval -1 didn't prevent compression buffering. Excluding the route from encode fixed the buffering behavior.The iframe hid a layout bug: long titles could run past the widget layout. A grid item's default min-inline-size: auto sets its floor at its own min-content, and a nowrap line of text will grow unbounded. Setting min-inline-size: 0 on a flex child inside it doesn't mitigate this — flex-shrink squeezes the column at layout time, but it doesn't shrink the grid item's min-content, so the track still grows to fit the title and nothing ever truncates. The fix is min-inline-size: 0 on the grid item itself, or minmax(0, 1fr) on the column. The iframe hid all of this because the stream document clips with overflow: hidden. Inline, there's nothing to clip against, so it just runs wide.View transition clashes: I animate album cover images when navigating from a page with one to the dedicated album page. I'm displaying the album cover in the now playing widget and may display it elsewhere on my home and music index pages. These transition names are derived using the album URL, but the browser aborts transitions when there's a duplicate name. So, if there's a copy of the same album cover outside of the widget, I remove the widget's transition and fall back to the default page-level transition.Navigating to an album animated, but navigating back didn't. The browser snapshots the incoming document at its first render, and on a back navigation the widget's fragment is still out on the wire. There's no cover in the document to pair with the album image, so the transition falls back to the default page transition. The fix was to cache the last fragment and repaint it before the first frame. This meant loading the widget's script as the only render-blocking script on the site. A deferred module only promises to run before DOMContentLoaded, which isn't the same as before the first paint.Then the fix broke it. The restored cover image is part of the transition, and a reconnecting client is sent the latest reading immediately, so the widget never goes blank. A fresh fragment landed a few milliseconds into a 300ms animation, replaced the element being morphed into, and the browser gave up and snapped to the end. It...

page widget transition album iframe listening

Related Articles