MyGo 云音乐
Go와 MyGo로 만든 서드파티 네이버 클라우드 뮤직 데스크톱 클라이언트
현재 재생곡·즐겨찾기·트렌드·장르·통계·인사이트 위젯을 한 페이지에 보여주는 Last.fm 대시보드
A single-page Last.fm dashboard: what you're playing right now, your favourite and trending tracks, a genre breakdown, lifetime stats, and a handful of fun data-driven insight widgets — all themed dyn
미디어PHP · JavaScriptGitHub @MichelleFindlay온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
A single-page Last.fm dashboard: what you're playing right now, your favourite and trending tracks, a genre breakdown, lifetime stats, and a handful of fun data-driven insight widgets — all themed dynamically from the current track's album art.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A single-page Last.fm dashboard: what you're playing right now, your favourite
and trending tracks, a genre breakdown, lifetime stats, and a handful of fun
data-driven insight widgets — all themed dynamically from the current track's
album art.README 전문 보기 ↗A single-page Last.fm dashboard: what you're playing right now, your favourite and trending tracks, a genre breakdown, lifetime stats, and a handful of fun data-driven insight widgets — all themed dynamically from the current track's album art.
config.sample.php and lib/ListenLinks.php). YouTube lookups
are capped at 100 per rolling 24 hours (youtube_daily_limit) to stay
clear of Google's free quota; excess lookups fall back to the search link.
With a YouTube API key, a Video button also appears when the track has
an official music video (no key, or no convincing match, and it stays
hidden). That's a second YouTube search per new track, from the same
daily cap.cache/The curl extension is used when available, with an automatic
allow_url_fopen fallback if it isn't (see lib/Http.php). The zlib
extension (bundled by default in almost every PHP build) is used to
compress the local library snapshot described below.
Copy the sample config and fill in your details:
cp config.sample.php config.php
Edit config.php and set at least api_key and username. Every other
setting has a sensible default — see the comments in config.sample.php
for what each one does (poll interval, cache TTLs, panel sizes, timezone,
widget sampling limits, etc.). config.php is gitignored so your API key
never gets committed.
Point a PHP-capable web server at the project root, or run PHP's built-in server for local testing:
php -S localhost:8000
Then open http://localhost:8000.
On shared/Apache hosting, .htaccess and .user.ini are included to
toggle PHP error display between debug and production — see the comments
in each file.
Widget and Lifetime Stats data is cached for 15 minutes. Left alone, that just means whoever loads the page after the cache expires triggers the refresh themselves — for most of the insight widgets that's fast (data is served from long-lived sub-caches), but a handful (Genre Breakdown, BPM Average, Obscurity Index, Before They Were Famous) sample deep enough into your library that a fully cold run can take a minute or more.
cron.php pre-warms all of that ahead of time, so visitors always land on
an already-cached page. Schedule it to run every 15 minutes:
# Real system cron (preferred if you have shell access — no execution-time
# limit imposed by a web server or reverse proxy to worry about):
0,15,30,45 * * * * php /full/path/to/lastfm-dash/cron.php >/dev/null 2>&1
If you're on shared hosting without shell access, most control panels offer a URL-based "cron job" feature instead:
0,15,30,45 * * * * curl -s "https://yourdomain.com/path/cron.php?token=YOUR_CRON_SECRET" >/dev/null
Note that a URL-triggered run is at the mercy of your web server's own
request timeout, which a very first (fully cold) run can exceed — real
system cron doesn't have that ceiling. Once you've scheduled either form,
set 'cron_enabled' => true in config.php so the dashboard shows a small
footer note confirming background refresh is active. If you set
'cron_secret', it's required as a ?token= query param for HTTP-triggered
runs (CLI runs are always allowed) — worth setting if this URL is easily
guessable and you'd rather not have random hits trigger an expensive run.
cron.php also grows a local, gzip-compressed copy of your full scrobble
history (artist + track + timestamp per scrobble) under cache/, so
Favourite Tracks, Trending, and Genre Breakdown can be computed straight
from that file for every period — with exact calendar boundaries instead
of Last.fm's approximate rolling windows, and with zero live API calls once
a given period is covered.
A large library can take a while to backfill in full: each cron run only
pulls a bounded batch (library_backfill_pages_per_run in config.php,
200 scrobbles per page, default 20 pages/run) rather than downloading
everything in one huge, rate-limit-risking request. New scrobbles since the
last run are always picked up cheaply on every run regardless of backfill
progress. Until a period's start date falls inside what's been backfilled,
that period transparently falls back to a live Last.fm call instead — nothing
breaks while backfill is still catching up, it's just not from the local
copy yet.
Genre Breakdown's local, uncapped view additionally needs Last.fm tags for
every distinct artist involved, which is its own paced background job —
library_tag_backfill_per_run (default 50/run), heaviest-played artists
first — so that too fills in gradually rather than needing a lookup burst
covering every artist you've ever scrobbled in one request.
This runs automatically as part of cron.php (see "Background cache
warming" above) — there's nothing extra to schedule.
With Spotify credentials configured (see "Quick-listen links" above), Genre Breakdown filters Last.fm's community tags down to Spotify's own genre vocabulary before scoring them, so tags that are really artist names, list titles, or one-off scrobbler noise ("My Top Songs", "Upcoming Album 2023") don't show up as if they were genres. A few common alternate spellings (Rnb/R&B, Dnb/Drum N Bass/Drum & Bass) are also collapsed onto one canonical entry.
Spotify retired its live genre-list endpoint, so this vocabulary
(SPOTIFY_GENRE_SEEDS in lib/LastFm.php) is a fixed, hardcoded list
matching what Spotify used to expose — meaning it's honest but dated: a few
genuinely common modern genre names (e.g. "pop punk", "screamo") aren't on
it and get filtered out along with the real noise. If that's cutting too
much for your taste, add the genre names you want recognized directly to
that list.
Without Spotify credentials configured, this filtering is skipped entirely
and every Last.fm tag (past GENRE_BLOCKLIST) is used as before.
mcp.php exposes this account's data as tools an MCP-compatible AI client
can call — MCP (Model Context Protocol) is a shared, model-agnostic
standard, so the same endpoint works for Claude, ChatGPT/OpenAI, or any
other client speaking it. Read-only: every tool just reads existing data,
nothing here can modify anything.
Setup: set mcp_api_key in config.php to a long random secret (it's
blank by default, which disables the endpoint entirely — every request
404s until you set one; generate one here
if you want a quick random value). Treat it like a password: don't paste
it into a chat, commit it, or share it — anyone with it can read your full
listening history through this endpoint.
Tools available:
| Tool | What it returns |
|---|---|
get_now_playing | Currently/most recently played track, plus the one before it |
list_scrobbles | Individual listening history — artist, track, exact date/time — with since/until/limit |
top_artists / top_tracks | Ranked by play count for a period (all_time/this_year/this_month/this_week/today) |
genre_breakdown | Genre percentages for a period, optionally Spotify-verified |
lifetime_stats | Scrobble/artist/album/track totals, average per day, member since |
widget_* | One tool per insight widget (widget_listening_clock, widget_distance, widget_bpm, widget_before_famous, etc.) |
list_scrobbles, top_artists, and top_tracks read from the local
library snapshot (see "Local library sync" above) when it covers the
requested range, honestly reporting "available": false with the current
backfill coverage if it doesn't yet — they never fall back to fabricated
or incomplete-looking data. top_artists/top_tracks/genre_breakdown
fall back to a live Last.fm call for the 5 named periods when local
history isn't there yet; list_scrobbles has no live equivalent for
arbitrary date ranges, so it just reports what's not covered.
Implements the request/response subset of MCP's Streamable HTTP transport
(initialize, tools/list, tools/call over a single POST, replying with
plain JSON) — not the optional SSE push stream, which a read-only tool
server like this one never needs to send anyway.
Connecting a client — every client needs the same two things: this
app's mcp.php URL, and your mcp_api_key sent as
Authorization: Bearer <your mcp_api_key> on every request.
A connector is added to your account once via claude.ai on the web or the Desktop app — it then shows up automatically everywhere you're signed in, mobile included, with nothing extra to set up on each device.
lastfm.mcp.php address, e.g. https://yourdomain.com/mcp.php.AuthorizationBearer YOUR_MCP_API_KEY — the literal word Bearer, a
space, then your key. The key by itself will not authenticate.If you don't see a Request headers section in the dialog, your account doesn't have that feature yet (it's newer and rolling out gradually) — use Claude Code below instead in the meantime, since Desktop's older OAuth-only connector flow has no way to send a custom API key at all.
claude mcp add --transport http lastfm-dash https://yourdomain.com/mcp.php \
--header "Authorization: Bearer YOUR_MCP_API_KEY"
Verify with claude mcp list.
The Responses API takes an mcp-type tool with a headers field for exactly
this kind of static-key auth — pass this in the tools array of your request:
{
"type": "mcp",
"server_label": "lastfm",
"server_url": "https://yourdomain.com/mcp.php",
"headers": { "Authorization": "Bearer YOUR_MCP_API_KEY" },
"require_approval": "never"
}
require_approval: "never" skips OpenAI's per-call confirmation prompt,
reasonable here since every tool is read-only. Headers are sent fresh with
every request rather than stored, so there's nothing to re-enter later if
you rotate the key — just update it in your own request code.
ChatGPT's own custom-connector UI (Settings → Connectors → Advanced → Developer mode, then Create) currently only offers OAuth or No authentication for a custom MCP server — there's no field for a static API key/Bearer header the way the API and Claude both support. Since setting "No authentication" would mean anyone with the URL can read your listening history, don't use that option here; the OpenAI API method above is the working path for OpenAI until the ChatGPT app supports custom headers. (Developer mode itself also needs a paid plan — Plus, Pro, Business, Enterprise, or Edu; not available on Free.)
The same URL, header name (Authorization), and value
(Bearer YOUR_MCP_API_KEY) work identically anywhere that lets you set a
custom request header — MCP's HTTP transport isn't tied to any one vendor.
GPL-3.0 — see LICENSE.