Skip to content

Commit 4123db6

Browse files
fhirschmannclaude
andcommitted
feat: folder cover-art fallback when no embedded ID3 cover
When the current track has no embedded APIC cover, /cover now looks for a sidecar image in the track's directory (cover.jpg/.jpeg/.png, folder.jpg/.png) and serves it before falling back to the placeholder music icon. Common for audiobooks/albums that ship a folder image instead of per-file embedded art. Verified on the test device: playing a no-art MP3 from a folder containing cover.jpg -> /cover returns image/jpeg (the folder image); placeholder still used when absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 96b83b7 commit 4123db6

2 files changed

Lines changed: 19 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
146146
| **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) |
147147
| **Now-playing info dialog**: an info icon next to "Current track" in the Control tab opens a dialog with the full track detail — ID3 title/artist/album, embedded cover, file path + size, play mode, track x/y, **codec / sample rate / bitrate / channels**, position/duration and the RFID tag that started the playlist. Served on demand via a new `GET /currenttrack` (so it doesn't bloat the frequent `trackinfo` websocket). Also: the **Unlocked** control button now uses the neutral (inactive) styling like the other toggles — only **Locked** keeps an accent colour. i18n de/en/fr | [`trackinfo-dialog`](../../releases/tag/trackinfo-dialog) |
148148
| **Settings tabs de-cluttered**: the long "General" sub-tab was split — the **LED** settings (NeoPixel brightness + LED config) moved into a new **LEDs** sub-tab, and **Deep sleep + Battery** into a new **Power** sub-tab. Both new tabs are covered by the same auto-save. General now holds just volume/branding/options/playlist/rotary. i18n de/en/fr | [`settings-split`](../../releases/tag/settings-split) |
149+
| **Folder cover-art fallback**: when a track has no embedded ID3 cover, `/cover` now serves a sidecar image from the track's folder (`cover.jpg`/`.jpeg`/`.png` or `folder.jpg`/`.png`) before falling back to the placeholder icon — so audiobooks/albums with a folder image show art in the Control tab | [`cover-fallback`](../../releases/tag/cover-fallback) |
149150
| **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) |
150151

151152
#### Feature highlights

‎src/Web.cpp‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3478,6 +3478,24 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) {
34783478
Log_Println("no cover image for webstream", LOGLEVEL_NOTICE);
34793479
request->send(200, "image/svg+xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><svg width=\"2304\" height=\"1792\" viewBox=\"0 0 2304 1792\" transform=\"scale (0.6)\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M784 1372l16-241-16-523q-1-10-7.5-17t-16.5-7q-9 0-16 7t-7 17l-14 523 14 241q1 10 7.5 16.5t15.5 6.5q22 0 24-23zm296-29l11-211-12-586q0-16-13-24-8-5-16-5t-16 5q-13 8-13 24l-1 6-10 579q0 1 11 236v1q0 10 6 17 9 11 23 11 11 0 20-9 9-7 9-20zm-1045-340l20 128-20 126q-2 9-9 9t-9-9l-17-126 17-128q2-9 9-9t9 9zm86-79l26 207-26 203q-2 9-10 9-9 0-9-10l-23-202 23-207q0-9 9-9 8 0 10 9zm280 453zm-188-491l25 245-25 237q0 11-11 11-10 0-12-11l-21-237 21-245q2-12 12-12 11 0 11 12zm94-7l23 252-23 244q-2 13-14 13-13 0-13-13l-21-244 21-252q0-13 13-13 12 0 14 13zm94 18l21 234-21 246q-2 16-16 16-6 0-10.5-4.5t-4.5-11.5l-20-246 20-234q0-6 4.5-10.5t10.5-4.5q14 0 16 15zm383 475zm-289-621l21 380-21 246q0 7-5 12.5t-12 5.5q-16 0-18-18l-18-246 18-380q2-18 18-18 7 0 12 5.5t5 12.5zm94-86l19 468-19 244q0 8-5.5 13.5t-13.5 5.5q-18 0-20-19l-16-244 16-468q2-19 20-19 8 0 13.5 5.5t5.5 13.5zm98-40l18 506-18 242q-2 21-22 21-19 0-21-21l-16-242 16-506q0-9 6.5-15.5t14.5-6.5q9 0 15 6.5t7 15.5zm392 742zm-198-746l15 510-15 239q0 10-7.5 17.5t-17.5 7.5-17-7-8-18l-14-239 14-510q0-11 7.5-18t17.5-7 17.5 7 7.5 18zm99 19l14 492-14 236q0 11-8 19t-19 8-19-8-9-19l-12-236 12-492q1-12 9-20t19-8 18.5 8 8.5 20zm212 492l-14 231q0 13-9 22t-22 9-22-9-10-22l-6-114-6-117 12-636v-3q2-15 12-24 9-7 20-7 8 0 15 5 14 8 16 26zm1112-19q0 117-83 199.5t-200 82.5h-786q-13-2-22-11t-9-22v-899q0-23 28-33 85-34 181-34 195 0 338 131.5t160 323.5q53-22 110-22 117 0 200 83t83 201z\"/></svg>");
34803480
} else {
3481+
// no embedded cover -> try a folder cover image (cover.jpg / folder.jpg / ...) next to the track
3482+
if (gPlayProperties.playlist && (gPlayProperties.playMode != NO_PLAYLIST) && (gPlayProperties.currentTrackNumber < gPlayProperties.playlist->size())) {
3483+
String track = gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber);
3484+
int slash = track.lastIndexOf('/');
3485+
if (slash >= 0) {
3486+
const String dir = track.substring(0, slash);
3487+
static const char *coverNames[] = {"/cover.jpg", "/cover.jpeg", "/cover.png", "/folder.jpg", "/folder.png"};
3488+
for (const char *name : coverNames) {
3489+
const String coverPath = dir + name;
3490+
if (gFSystem.exists(coverPath)) {
3491+
const char *mime = coverPath.endsWith(".png") ? "image/png" : "image/jpeg";
3492+
Log_Printf(LOGLEVEL_NOTICE, "serve folder cover: '%s'", coverPath.c_str());
3493+
request->send(gFSystem, coverPath, mime);
3494+
return;
3495+
}
3496+
}
3497+
}
3498+
}
34813499
// no cover -> send placeholder icon for playing music from SD-card (fa-music)
34823500
if (gPlayProperties.playMode != NO_PLAYLIST) {
34833501
Log_Println("no cover image for SD-card audio", LOGLEVEL_DEBUG);

0 commit comments

Comments
 (0)