Skip to content

Commit ce3116c

Browse files
fhirschmannclaude
andcommitted
feat: parse + show ID3 artist & album in the now-playing (Control) tab
The Control tab already showed the ID3 title + embedded cover; add artist and album: - AudioPlayer: parse 'Artist'/'ARTIST=' (TPE1) and 'Album'/'ALBUM=' (TALB) in the evt_id3data handler via a small Audio_SetMeta() helper (value after ':'/'='), stored in new gPlayProperties.artist/album. Cleared per track in AudioPlayer_ClearCover() so a tag-less track shows no stale metadata. - Web: trackinfo websocket payload gains artist/album. - UI: a #trackmeta line under the title shows 'Artist — Album' (omits empty parts). Verified on the test device with a generated MP3 carrying safe ID3 tags (uploaded, played, removed): the final trackinfo carries both artist and album and the Control tab renders 'LEO TestArtist — LEO TestAlbum'. Note: the existing title parser keeps a pre-existing leading space (unchanged here). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2e608f9 commit ce3116c

5 files changed

Lines changed: 44 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
142142
| **Listening-time statistics**: tracks how long was actually listened per local calendar day in a 365-day ring buffer (persisted in NVS, ~1.5 KB). The info dialog (ℹ️) shows **today / yesterday / last 7 days / last 30 days** next to the existing total playtime. Seconds are accumulated by the audio loop while playing (clock from NTP/RTC; ignored until the clock is valid), flushed to NVS at most once a minute and on shutdown. New `/info` audio fields `playToday`/`playYesterday`/`play7d`/`play30d` (seconds). i18n de/en/fr | [`listening-stats`](../../releases/tag/listening-stats) |
143143
| **RFID-tag syncing (server + peer-to-peer)**: keeps RFID-tag assignments in sync across a central PHP server *and* other ESPuinos at the same time. On **learn** a freshly assigned card is pushed to the server and to every configured peer; a **full sync** (button in the Sync settings tab, bindable command **187**, or MQTT `rfid_sync`) pulls the server list, merges newest-wins per tag id (by timestamp) and pushes every local tag back to server + peers. A **catch-up** sync runs automatically once after the device comes online, so a player used **offline/outdoors** (no WiFi, no server) keeps working — cards learned offline are stored locally and pushed up on reconnect (assignments made while the clock was never set are healed to "now" on first sync so they win). Conflict resolution is **newest-wins** by timestamp. Two dynamic server scripts (no static manifest.json): [`server/manifest.php`](server/manifest.php) lists the files for the file sync, [`server/rfid.php`](server/rfid.php) is the stateful RFID-tag store — both are linked from the Sync settings tab. Peers are managed in a small **add-peer editor** (IP/hostname + password, with a reveal toggle) and stored as JSON; since ESPuinos are password-protected, the peer push authenticates with each peer's password via the `X-API-Key` header (falling back to this device's web password). The Sync settings tab is split into **File sync** and **RFID-tag sync** sections (each Save button disabled until its URL is filled), and both runners (file sync + RFID full sync) live in the **Tools** tab. The file browser badges a file/folder (after the name) when it has an RFID assignment (tag icon) or a per-path EQ rule (sliders icon), and the explorer context menu can assign/remove both. i18n de/en/fr | [`rfid-sync`](../../releases/tag/rfid-sync) |
144144
| **Configurable branding + RFID settings tidy-up**: the navbar **header** title and the **footer** brand are now configurable (General → Branding) with a **live preview** as you type — empty keeps the "Leo Industries" default, so the fork is easy to re-brand. The Sync settings tab is split into two clear sections (**File sync** and **RFID-tag sync**). The RFID **reader** settings (reader type, MFRC522 gain, PN5180 LPCD, SLIX2 password) moved out of the General tab into the **RFID** settings sub-tab where they belong, next to the card-learning UI. Both are covered by the General/RFID-tab auto-save. i18n de/en/fr | [`ui-branding`](../../releases/tag/ui-branding) |
145+
| **Now-playing: ID3 artist + album**: the Control tab already showed the ID3 **title** and embedded **cover** (APIC); it now also parses the **artist** (`TPE1`/`ARTIST=`) and **album** (`TALB`/`ALBUM=`) tags and shows them as "Artist — Album" under the title. The fields are reset per track (so a tag-less track doesn't show stale metadata) and exposed in the `trackinfo` websocket payload (`artist`/`album`) | [`id3-meta`](../../releases/tag/id3-meta) |
145146
| **OpenAPI/Swagger spec updated**: the bundled `REST_API.yaml` (served at `/swagger.html`) now documents all fork-added endpoints (`/sync`, `/syncstop`, `/playlist`, `/githubupdate`, `/version`, `/login`, `/logout`, `/security`, `/eqrule(s)`, `/activeequalizer`, `/bluetooth*`, `/sdclean`) and declares the security schemes (`X-API-Key` header / `apikey` query param, or the `ESPUINO_SESSION` login cookie) so the Swagger "Authorize" button works. The web-interface footer links to the Swagger UI (`/swagger.html`) | [`f8298d9`](../../commit/f8298d9) |
146147

147148
#### Feature highlights

‎html/management.html‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1936,7 +1936,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
19361936
<div class="mb-3 col-md-12">
19371937
<legend data-i18n="control.current"></legend>
19381938
<div id="track"></div>
1939-
<div></div>
1939+
<div id="trackmeta" class="text-secondary small mb-1"></div>
19401940
<div id="trackProgressDiv" class="progress progress-sm">
19411941
<div id="trackProgress" class="progress-bar" role="progressbar" aria-valuenow="0"
19421942
aria-valuemin="0" aria-valuemax="100"></div>
@@ -4508,6 +4508,13 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
45084508
}
45094509
if ("trackinfo" in socketMsg) {
45104510
document.getElementById('track').innerHTML = socketMsg.trackinfo.name;
4511+
// ID3/Vorbis artist + album (if present): shown as "Artist — Album" under the title
4512+
var tmeta = document.getElementById('trackmeta');
4513+
if (tmeta) {
4514+
var artist = (socketMsg.trackinfo.artist || "").trim();
4515+
var album = (socketMsg.trackinfo.album || "").trim();
4516+
tmeta.textContent = [artist, album].filter(Boolean).join(" — ");
4517+
}
45114518
setTrackProgress(socketMsg.trackinfo);
45124519
if ("controlsLocked" in socketMsg.trackinfo) {
45134520
updateLockControlsState(socketMsg.trackinfo.controlsLocked);

‎src/AudioPlayer.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ static void AudioPlayer_SortPlaylist(Playlist *playlist);
107107
static void AudioPlayer_RandomizePlaylist(Playlist *playlist);
108108
static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed);
109109
static void AudioPlayer_ClearCover(void);
110+
static void Audio_SetMeta(char *dest, size_t destSize, const char *line); // store an ID3/Vorbis field value (after ':'/'=') + notify the web UI
110111
static void audio_id3image(File &file, const size_t pos, const size_t size);
111112
static void audio_oggimage(File &file, std::vector<uint32_t> v);
112113

@@ -180,6 +181,12 @@ void Audio_InfoCallback(Audio::msg_t m) {
180181
Audio_setTitle("%s", m.msg + titleStart);
181182
}
182183
}
184+
// get artist / album (ID3 "Artist:"/"Album:" + VORBISCOMMENT "ARTIST="/"ALBUM=")
185+
else if (startsWith((char *) m.msg, "Artist") || startsWith((char *) m.msg, "ARTIST=") || startsWith((char *) m.msg, "artist=")) {
186+
Audio_SetMeta(gPlayProperties.artist, sizeof(gPlayProperties.artist), (const char *) m.msg);
187+
} else if (startsWith((char *) m.msg, "Album") || startsWith((char *) m.msg, "ALBUM=") || startsWith((char *) m.msg, "album=")) {
188+
Audio_SetMeta(gPlayProperties.album, sizeof(gPlayProperties.album), (const char *) m.msg);
189+
}
183190
break;
184191
}
185192
case Audio::evt_lasthost: { // stream URL played
@@ -531,6 +538,26 @@ void Audio_setTitle(const char *format, ...) {
531538
#endif
532539
}
533540

541+
// Store an ID3/Vorbis metadata field (e.g. "Artist: X" or "ARTIST=X"): copies the value after the
542+
// first ':' or '=' delimiter (leading spaces trimmed) into dest, then notifies the web UI.
543+
static void Audio_SetMeta(char *dest, size_t destSize, const char *line) {
544+
const char *v = strchr(line, '=');
545+
if (!v) {
546+
v = strchr(line, ':');
547+
}
548+
if (v) {
549+
v++;
550+
while (*v == ' ') {
551+
v++;
552+
}
553+
} else {
554+
v = "";
555+
}
556+
strncpy(dest, v, destSize - 1);
557+
dest[destSize - 1] = '\0';
558+
Web_SendWebsocketData(0, WebsocketCodeType::TrackInfo);
559+
}
560+
534561
// Set maxVolume depending on headphone-adjustment is enabled and headphone is/is not connected
535562
// Enable/disable PA/HP-amps initially
536563
void AudioPlayer_SetupVolumeAndAmps(void) {
@@ -1706,6 +1733,10 @@ void AudioPlayer_SortPlaylist(Playlist *playlist) {
17061733
void AudioPlayer_ClearCover(void) {
17071734
gPlayProperties.coverFilePos = 0;
17081735
AudioPlayer_StationLogoUrl = "";
1736+
// reset per-track metadata too (called at the start of every track), so artist/album from a
1737+
// previous track don't linger if the new one carries no ID3/Vorbis tags
1738+
gPlayProperties.artist[0] = '\0';
1739+
gPlayProperties.album[0] = '\0';
17091740
// websocket and mqtt notify cover image has changed
17101741
Web_SendWebsocketData(0, WebsocketCodeType::CoverImg);
17111742
#ifdef MQTT_ENABLE

‎src/AudioPlayer.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ typedef struct { // Bit field
2222
uint8_t playMode; // playMode
2323
Playlist *playlist; // playlist
2424
char title[255]; // current title
25+
char artist[255]; // current artist (from ID3/Vorbis tags, empty if none)
26+
char album[255]; // current album (from ID3/Vorbis tags, empty if none)
2527
bool repeatCurrentTrack : 1; // If current track should be looped
2628
bool repeatPlaylist : 1; // If whole playlist should be looped
2729
uint16_t currentTrackNumber : 9; // Current tracknumber

‎src/Web.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2252,6 +2252,8 @@ void Web_SendWebsocketData(uint32_t client, WebsocketCodeType code) {
22522252
entry["numberOfTracks"] = (gPlayProperties.playlist) ? gPlayProperties.playlist->size() : 0;
22532253
entry["volume"] = AudioPlayer_GetCurrentVolume();
22542254
entry["name"] = gPlayProperties.title;
2255+
entry["artist"] = gPlayProperties.artist; // ID3/Vorbis artist (empty if none)
2256+
entry["album"] = gPlayProperties.album; // ID3/Vorbis album (empty if none)
22552257
entry["posPercent"] = gPlayProperties.currentRelPos;
22562258
entry["playMode"] = gPlayProperties.playMode;
22572259
entry["controlsLocked"] = System_AreControlsLocked();

0 commit comments

Comments
 (0)