Skip to content

Releases

v1.7.2

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 25 Oct 10:59

This release also includes v1.7.1, which didn't get a release of its own.

Added (v1.7.1)

  • Every page and endpoint now sends a Cross-Origin-Embedder-Policy header.
    • The dashboard uses credentialless, so album art from Last.fm, Spotify, Apple, Deezer and Wikipedia still loads, just without cookies. The stricter require-corp would block those images.
    • The JSON endpoints use require-corp, since they never load anything from other sites.

Changed (v1.7.2)

  • The footer's DNSSEC and security-header checks now start with an icon: 🔐 DNSSEC and 🛡️ Security headers.
  • The pass/fail mark (✓, ✗, or – if the check couldn't run) now comes after each label instead of before it.

v1.7.0

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 24 Oct 19:19

Security headers (A+ on securityheaders.com)

Every page and endpoint now sends security headers from PHP. You don't need to change your web server config, so this works on shared hosting too.

  • Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Content-Security-Policy: only scripts carrying a per-request nonce can run, so the policy doesn't need 'unsafe-inline' for scripts. Album art can still load from any HTTPS host.
  • X-Frame-Options: SAMEORIGIN
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: turns off camera, microphone, geolocation, payment, USB and motion sensors
  • Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy: same-origin
  • PHP's X-Powered-By header is removed, so the response no longer shows your PHP version.
  • The JSON endpoints (api.php, widgets.php, films.php, listen_links.php, mcp.php) send a policy that blocks everything, since they never render a page.

Footer: DNSSEC and security-header checks

A new line under "Powered by" shows a tick or cross for:

  • DNSSEC: whether your domain's DNS records are signed and validate. The result is checked through Cloudflare's public DNS resolver, cached for 24 hours, and links to Verisign's DNSSEC Analyzer.
  • Security headers: whether all six headers that securityheaders.com grades are being sent. Links to a securityheaders.com scan of your site.

The line is hidden on localhost or when the site is reached by IP address.

Fixes

  • Broken thumbnails on Favourite Tracks and Trending rows still fall back to the letter placeholder. The fallback used an inline onerror attribute, which the new policy blocks, so it now lives in assets/app.js.

Upgrade notes

  • Behind Cloudflare? Cloudflare features that add their own scripts to the page will be blocked by the new policy. These include Rocket Loader, Email Address Obfuscation and automatic Web Analytics. Turn them off in your Cloudflare dashboard if they're on.
  • HSTS tells browsers to use only HTTPS for your domain and all its subdomains for a year. Make sure HTTPS works everywhere on the domain before you deploy.
  • You don't need to change config.php.

v1.6.9

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 10 Oct 18:08

New

Music video button. A red Video button now appears next to the listen links when a track has an official music video on YouTube, and opens it in a new tab. It shows in the Now Playing card, as a small icon under "Previously played", and in the icons that appear when you hover over a track row.

The button needs a YouTube API key (youtube_api_key in config.php). Without one, it stays hidden. The button only appears when a YouTube result names the track and comes from the artist: their name is in the title, or it's their own or VEVO channel. YouTube's auto-generated "Artist - Topic" audio uploads don't count. Neither do lyric videos, audio-only uploads, covers, live versions or remixes, unless the scrobbled track is that version itself. If nothing matches clearly, you get no button rather than a link to the wrong video. Results are cached for 30 days per track.

Fixed

  • On phones, the listen buttons now wrap onto a second line instead of pushing the Amazon Music button off the edge of the screen.

Upgrading

  • No config changes are needed.
  • With a YouTube API key set, each new track now uses two YouTube searches instead of one: one for the YouTube Music link and one for the video. Both count toward the same youtube_daily_limit, so roughly half as many tracks a day get verified YouTube links before falling back to search links. If you have quota to spare, you can raise the limit. Google's default free quota is about 100 searches a day.

Known limitations

  • Official videos with unusual titles, or ones posted on a label's channel that doesn't name the artist, may not be picked up.

v1.6.8

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 09 Oct 19:07

New

Sales certification badges. Tracks certified in the UK (BPI) or US (RIAA) now show a small record-disc icon, coloured by the highest level reached: Silver, Gold, Platinum or Diamond. Multi-platinum and multi-diamond tracks also show the multiplier (e.g. 12×). Hover the disc to see each region's award and unit count:

UK (BPI): 12× Platinum · 7,200,000 units
US (RIAA): Diamond · 10,000,000 units

Certifications come from the certification tables on English Wikipedia song articles, which cite BPI and RIAA directly; neither body offers a public API. A track only gets a badge when the article clearly matches both the track and the artist, and cover versions on the same page are ignored. RIAA Latin certifications are labelled "(Latin)" and never outrank a standard certification.

Explicit marker. Explicit tracks get the familiar boxed E, based on Apple Music's and Spotify's explicit flags.

"In films" on hover. Album-art tooltips now list the films a track appears in, taken from the soundtrack albums it's on in MusicBrainz. Films load the first time you hover a track and are then cached.

Listen links for the previous track. The "Previously played" card now has small Spotify, YouTube Music, Apple Music and Amazon Music icons.

"Powered by" footer. The footer now credits the services the dashboard uses: Last.fm, Spotify, Apple Music, Deezer, YouTube, MusicBrainz and Wikipedia.

Fixed

  • Apple Music and Spotify searches no longer cache a rate-limit or error response as "no match" for 30 days. Before, this could leave tracks without album art or listen links for a month after a busy cold-cache run.

Upgrading

  • No config changes are needed.
  • The server now also connects to musicbrainz.org and en.wikipedia.org. If your host restricts outbound requests, allow both.
  • Each track is looked up once more on Apple Music/Spotify to pick up its explicit flag, and once on Wikipedia for certifications. The first page loads after upgrading may be slower. Running cron.php warms these lookups in the background.
  • Film lookups are kept under MusicBrainz's limit of one request per second, so a track's films can take a moment to appear the first time you hover it.

Known limitations

  • Film data only covers songs that appear on a film's soundtrack album. Some TV and compilation soundtracks may still slip through.
  • Certifications only cover tracks that have an English Wikipedia article. Only the UK and US are shown.

v1.6.7

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 09 Oct 16:36
1ba032f

Added

  • Apple Music listen links — resolved via Apple's free iTunes Search API (no credentials needed), with artist-name verification to avoid linking the wrong same-titled track.
  • Amazon Music listen links — always a plain search link, since Amazon has no public track-search API to verify against.
  • Album art fallback chain — when Last.fm has no art for a track (common on soundtrack/compilation scrobbles), falls back to Spotify, then Apple Music, before giving up. Applies to the Now Playing hero, Previously Played thumb, and every Favourite Tracks/Trending row.
  • Grammy Award badge — a small trophy icon on any track that's won a Grammy, resolved via grammy.com's own public artist search and per-artist nominations table. Deliberately conservative: only exact artist/track matches count, so a near-miss shows nothing rather than a wrong claim.
  • "Loved on Last.fm" heart badge — shown on the Now Playing hero, Previously Played card, and every track row, with a tinted circular badge so it stands out.

Fixed

  • The art hover-tooltip on the Now Playing hero and Previously Played thumb no longer opens above the top of the viewport and gets clipped — it now flips downward when there isn't room above.
  • A Chromium rendering bug where the Favourite Tracks/Trending panel's heading bled through that same tooltip whenever they overlapped (two backdrop-filter layers interacting badly) — fixed by briefly suppressing the blur on both sides while the tooltip is open, instead of reserving layout space or fighting z-index.

Performance

  • Cold-cache Favourite Tracks/Trending loads were taking up to 56 seconds once Grammy/art-fallback lookups were added per track. cron.php's existing pre-warming was extended to cover every period, bringing a warmed load down to 0.3 seconds.

Internal

  • Spotify's track search is now shared between the listen-link button and album-art fallback resolution (one API call instead of two per track).
  • Added Http::postJson() for JSON request bodies (previously form-encoded only).

v1.6.5

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 26 Sep 15:53
ddf7008

New features

  • MCP server (mcp.php) — expose this account's data as tools an MCP-compatible AI client (Claude, ChatGPT/OpenAI, or anything else speaking the standard) can call: get_now_playing, list_scrobbles (individual listening history with exact dates/times), top_artists, top_tracks, genre_breakdown, lifetime_stats, and one widget_* tool per insight widget. Gated behind a bearer API key (mcp_api_key in config.php, blank/disabled by default). Falls back honestly — "available": false plus current backfill coverage — rather than fabricating data when the local history doesn't reach far enough back yet.
  • Local library sync — cron.php now builds a compressed local snapshot of your full scrobble history (artist + track + timestamp), paced in bounded batches across runs. Once a period is covered, Favourite Tracks/Trending/Genre Breakdown compute from it directly with exact calendar boundaries (real Jan 1, real 1st-of-month, real Monday) instead of Last.fm's approximate rolling windows, and with zero live API calls.
  • Genre Breakdown, rebuilt: once local history covers a period, it scores every distinct artist scrobbled (not a capped sample) with no "Other" catch-all. Optionally filtered down to Spotify's own genre vocabulary when Spotify credentials are configured, collapsing near-duplicate spellings (Rnb/R&B, Dnb/Drum N Bass/Drum & Bass) onto one entry. A "Show: all / above 1% / 2% / 5%" dropdown keeps a long genre list from making the page too tall.
  • Before They Were Famous, verified — each candidate is checked against Spotify's own artist search before inclusion, filtering out soundtrack/compilation scrobbles where the "artist" is really an album or production title (e.g. "Monster High the Movie"). Both this widget and Obscurity Index also now filter out multi-artist scrobble credits ("Artist A, Artist B & Artist C"), which Last.fm otherwise treats as one low-listener "artist".
  • Distance Listened — each comparison now shows a progress bar for the fractional remainder (e.g. "~74 flights, 64%") instead of a bare decimal.
  • BPM Average — the plain pulsing dot is now an animated ECG-style heart-monitor trace, scrolling at the real average-BPM's beat interval.
  • YouTube quota protection — a local 24-hour call counter (youtube_daily_limit, default 100) so quick-listen lookups can't exceed the free API quota.

Fixes

  • ListenLinks caching bug: a failed Spotify/YouTube request (auth hiccup, network error) was being cached as a permanent "no match" for 30 days — indistinguishable from a genuine miss. Now only a completed API response gets cached, so a transient failure retries fresh next time instead of freezing a track's button on "Search" for a month.

Architecture

  • Genre tag scoring and Spotify's Client Credentials auth are now shared, reusable pieces (lib/Spotify.php), used by both quick-listen links and the new artist/genre verification.
  • LibrarySync's artist/track query methods are now public with an optional upper time bound, plus new rawScrobbles()/coverage() methods for arbitrary date-range queries — the foundation the MCP server's list_scrobbles tool is built on.
  • Genre tag coverage (for the uncapped local Genre Breakdown) and scrobble history backfill both grow via their own paced background jobs in cron.php, never a live lookup burst.

Documentation

  • README additions: Local library sync, Genre verification (including an honest note that Spotify's genre vocabulary is a fixed, dated list since Spotify retired the live endpoint), and a full MCP server section with step-by-step connection instructions for the Claude app, Claude Code, the OpenAI API, and ChatGPT's current OAuth-only limitation.

v1.6.0

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 26 Sep 15:13
140ba60

New features

  • Genre Breakdown, Spotify-verified: filter Last.fm's community tags down to Spotify's own genre vocabulary (SPOTIFY_GENRE_SEEDS) when Spotify credentials are configured, so tags that are really artist names, list titles, or one-off scrobbler noise don't show up as genres. Common near-duplicate spellings (Rnb/R&B, Dnb/Drum N Bass/Drum & Bass) are collapsed onto one canonical entry.
  • Genre Breakdown, less noisy: a "Show: all / above 1% / 2% / 5%" dropdown hides small legend rows (the bar itself stays untouched) so a full genre scan doesn't make the page too long. The selected threshold persists as you switch periods.
  • Before They Were Famous, verified: each candidate is checked against Spotify's own artist search before being included, filtering out soundtrack/compilation scrobbles where the "artist" field is really an album or production title (e.g. "Monster High the Movie"). Spotify's Client Credentials auth was extracted into a shared lib/Spotify.php, reused by both this and the existing quick-listen links.
  • Multi-artist credit filtering: Before They Were Famous and Obscurity Index now both filter out multi-artist scrobble credits ("Artist A, Artist B & Artist C"), which Last.fm otherwise treats as one low-listener "artist" — a source of false "obscure find" results.
  • YouTube quota protection: a local 24-hour call counter (youtube_daily_limit, default 100) caps live YouTube API lookups so quick-listen links can't exceed the free quota; excess lookups fall back to the search link.
  • Distance Listened, at a glance: each comparison (flights, marathons, Moon/Mars trips, etc.) now shows a progress bar for the fractional remainder — e.g. "~74 flights, 64%" — instead of a bare decimal count.
  • BPM Average, visualized: replaced the plain pulsing dot with an animated ECG-style heart-monitor trace, scrolling at the real average-BPM's beat interval.

Fixes

  • ListenLinks caching bug: a failed Spotify/YouTube request (auth hiccup, network error) was being cached as a permanent "no match" for 30 days — indistinguishable from a genuine miss. Now only a completed API response gets cached, so a transient failure retries fresh next time instead of freezing a track's button on "Search" for a month.

v1.5.0

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 25 Sep 21:25
67e9786

New features

  • Spotify / YouTube Music listen links — every track now gets an auto-generated Spotify and YouTube Music link, shown on the current track and on hover over any track row. Works out of the box as plain search links; adding optional API credentials upgrades them to verified direct links to the exact track.
  • Previously-played track tile — a compact card next to the now-playing hero shows the track before whichever one is currently displayed (the last-played track when something's playing, or the 2nd-last when nothing is), with its own art, name, and artist, updated on every poll.
  • "This Week" period option — added alongside All Time / This Year / This Month / Today on the Favourite Tracks, Trending, and Genre Breakdown period pickers, using Last.fm's rolling 7-day window.
  • Widget and stat icons — added small icon glyphs (Lucide, inline SVG, no runtime CDN dependency) to each insight widget card and lifetime stat.

Fixes

  • Hover icons on track rows were unclickable — the play count faded out on hover but kept pointer-events, silently intercepting clicks meant for the Spotify/YouTube icons underneath.
  • Play count wrapping onto its own line — fixed by giving it explicit CSS Grid placement instead of relying on auto-placement.
  • Dark theme not covering the page below the viewport on pages taller than one screen.
  • Theme not resetting to default when a track has no album art — accent color now properly reverts instead of sticking to the last art's color.
  • Duplicate "previously played" track — Last.fm can keep a track marked "now playing" even after it's crossed its own scrobble threshold, so the same track could appear twice in the recent-tracks list. The previous-track lookup now skips past any leading duplicate of the current track.
  • Dead album art URLs — Last.fm's API can list an artwork size that 404s on its own CDN even when smaller sizes of the same image work. Every piece of art (hero tiles, track list thumbnails, server- and AJAX-rendered) now falls back to the existing letter-placeholder UI on a failed image load instead of showing a broken image icon.
  • Footer GitHub link had no visible text, only an icon.

Changes

  • Collapsed the now-playing hero to a single art tile. Testing showed Last.fm's album.getinfo always returns the identical image as the track's own art, so the separate Track/Album tiles were showing duplicate images. Removing the second tile also drops that now-pointless API call entirely.
  • Version tracking moved out of config.php into a plain VERSION file tracked in git, since it describes the codebase itself rather than a per-install setting.
  • Genre Breakdown pre-warmed in cron.php for every period the picker offers — it's the main page's slowest cold-cache path (one artist.gettoptags call per top artist, per period), so background pre-warming now covers it the same way the other insight widgets already were.

Other

  • Added a LICENSE file (GPL-3.0).

v1.0.0

Choose a tag to compare

@MichelleFindlay MichelleFindlay released this 25 Sep 17:15

A single-page, self-hosted Last.fm dashboard with live theming, period-switchable stats, and a set of data-driven insight widgets.

✨ Highlights

  • Now Playing hero — live "now scrobbling" / last-played track, polled every ~10 seconds, with independent Track and Album artwork tiles (they're fetched separately and can differ).
  • Dynamic theming — the page background and accent colour are extracted live from the current track's album art and eased in over a smooth transition. The accent colour is clamped to a safe WCAG luminance band so text stays readable regardless of how light or dark the source artwork is.
  • Favourite Tracks, Trending, and Genre Breakdown panels, each with an All Time / This Year / This Month / Today period picker, switched instantly via AJAX with no page reload.
  • Lifetime Stats — total scrobbles, unique artists/albums/tracks, average scrobbles/day, and member-since date, ticking up live as new scrobbles land rather than only refreshing on page load.
  • Eight insight widgets, opened as popups from clickable cards:
    • Listening Clock — a 24-hour radial chart of when you actually listen, labeled by dominant period (e.g. "Night Owl", "Dawn Chorus")
    • Energy Curve — real scrobble-activity volume across the week (clearly labeled as activity, not audio tempo — Last.fm has no tempo data)
    • Distance Listened — estimated total listening time converted into flights, marathons, laps of the Earth, and — for heavy listeners — trips to the Moon or Mars
    • If Your Year Were a Festival — your top artists billed as a tiered festival poster lineup
    • Mood Weather — a monthly emotional "forecast" derived from your top artists' community tags
    • BPM Average — real average tempo sourced from Deezer's free public API, since Last.fm exposes none
    • Before They Were Famous — your favourite artists with the lowest current global Last.fm listener counts
    • Obscurity Index — the average and median global listener count across your top artists
  • Self-update check — the footer compares the installed version against the latest GitHub release and links to it when an update is available, with a GitHub icon/link alongside the version number.

🔧 Under the hood

  • File-based caching throughout, with TTLs tuned per data type (seconds for now-playing polling, a day for genre/track-art lookups, a week for artist metadata) — most repeat page loads resolve in single-digit milliseconds.
  • Graceful curl → file_get_contents fallback for hosts without the curl extension.
  • .htaccess / .user.ini included for toggling PHP error display between debug and production on shared hosting.
  • All external API calls (Last.fm, Deezer, GitHub) are cached independently and fail gracefully with clear empty/error states rather than fatal errors.

📄 Documentation & Licensing

  • Added a full project README.md describing setup, requirements, and every feature above.
  • Licensed under GPL-3.0 (LICENSE).

🐛 Fixes

  • Corrected the dynamic accent-color extraction so text always remains legible against album-art-derived backgrounds and buttons, regardless of how light, dark, or saturated the source image is.