v0.8.0: export a playlist — as a folder, or as a whole website

File → Export Playlist…, and the same item on a playlist's right-click
menu. A sibling of device_sync: pure plan_export() first, then a worker
on a daemon thread sharing the status-bar progress widgets. The manifest
(index.html / .m3u) is written last, so an interrupted export never
leaves a page naming files that aren't there.

Two destinations. A folder gives the audio under "Artist - Title" names
plus an extended .m3u. A web mix gives a self-contained static site
reproducing the hand-made yearly mixes, with a dialog for title,
description and hero image.

audio.js and jQuery are gone. The old pages shipped ~293 KB: a build of
audio.js whose upstream hasn't moved since 2012, plus jQuery 3.2.1
(CVE-2019-11358, CVE-2020-11022, CVE-2020-11023 — unexploitable on a
static page, since nothing untrusted reaches a jQuery HTML sink, but
dead weight regardless). audio.js never used jQuery; jQuery was there
for ~25 lines of tracklist glue. Both are replaced by a dependency-free
player.js plus a player.css transcribed from the customized audio.js
skin, so the page looks identical — same 250px #c7b563 bar, same
player-graphics.gif (byte-identical: it's an animated GIF whose loading
frame is a spinner), same shortcuts. ~293 KB → ~6 KB. The Flash
fallback went too; audio.js gated it on !canPlayType("audio/mpeg;"),
unreachable since ~2010.

Conversion happens only when a browser genuinely can't decode a file,
never because of bitrate — a 320 kbps MP3 is copied verbatim. Everything
that does convert targets FLAC, so a conversion cannot cost a bit; a
test asserts the exported FLAC's decoded PCM hashes identical to the
ALAC source. Against the real library that's 105 Apple Lossless and 8
AIFF out of 21,382 tracks. DRM'd tracks are reported in a confirmation
dialog, never silently dropped and never attempted. ffmpeg is the CLI
binary here, not Qt's ffmpeg backend, so it's detected at runtime.

Verified in Chromium 151 and Firefox 153 over CDP/Marionette: every
exported format decodes including the converted FLAC, the player builds,
click / space / arrows / scrubber-seek / autoplay-next all work, and the
network log shows no request for jquery, audio.min.js or any .swf.

mix-example/ removed; the template reproduces it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:03:01 -04:00
co-authored by Claude Opus 5
parent 211cb50a9e
commit afadf31a99
17 changed files with 1781 additions and 4 deletions
+53
View File
@@ -1,5 +1,58 @@
## Done
### Round 37 (2026-08-19) — Export a playlist: a folder, or a whole website (v0.8.0)
`File → Export Playlist…`, and the same item on a playlist's right-click menu.
Two destinations behind one planner, built as a sibling of `device_sync` (same
shape: a pure `plan_export`, then a worker on a daemon thread reporting through
signals, sharing the status-bar progress widgets and the cancel button).
- [x] **Folder export** — the audio files under `Artist - Title.ext` names next
to an extended `.m3u`. No `.pls`; nothing reads it any more. Files are
copied byte-for-byte, never re-encoded.
- [x] **Web mix export** — a self-contained static site reproducing the
hand-made yearly mixes: `index.html`, `audios/`, a hero image, and a
dialog for title / description / image. The description is deliberately
passed through raw (the mixes lean on inline links right there); the title
and track labels are escaped. Per-track liner notes stay hand-edited —
`index.html` carries a commented-out block in the right spot.
- [x] **audio.js and jQuery are gone.** The old pages shipped ~293 KB: a build of
audio.js whose upstream hasn't moved since 2012, plus jQuery 3.2.1
(CVE-2019-11358, CVE-2020-11022, CVE-2020-11023 — not exploitable on a
static page, since nothing untrusted ever reaches a jQuery HTML sink, but
dead weight either way). The decisive detail: **audio.js never used
jQuery** — jQuery was there for ~25 lines of tracklist glue. Replaced by a
~180-line dependency-free `player.js` + a `player.css` transcribed from
the customized audio.js skin, so the page looks the same: same 250px
`#c7b563` bar, same `player-graphics.gif` (shipped byte-identical — it's
an *animated* GIF, the loading state is a spinner), same click-to-play,
autoplay-next and ←/→/space shortcuts. **~293 KB → ~6 KB.** The Flash
fallback went with it; audio.js gated it on `!canPlayType("audio/mpeg;")`,
unreachable in any browser since ~2010.
- [x] **Conversion only when a browser genuinely can't play it, and never lossy.**
`export/web_support.py` is a deny-by-default gate modelled on
`cast/support.py`, using the iTunes `kind` string to disambiguate `.m4a`
(plain AAC / Apple Lossless / FairPlay all wear that suffix). Bitrate is
**not** a trigger — a 320 kbps MP3 is copied verbatim. Everything that
does convert targets **FLAC**, so a conversion cannot cost a bit; a test
asserts the exported FLAC's decoded PCM hashes identical to the ALAC
source. Against the real library that is 105 Apple Lossless + 8 AIFF out
of 21,382 tracks. DRM'd tracks (6 Protected AAC) are reported in a
confirmation dialog, never silently dropped and never attempted.
- [x] **ffmpeg is detected, not assumed** — it's the CLI binary, which is a
different thing from Qt Multimedia's ffmpeg backend. Missing, the export
offers to go ahead without the affected tracks.
- [x] **The manifest is written last.** A cancelled or interrupted export leaves
no `index.html` and no `.m3u`, so a half-finished folder never advertises
files that aren't there. Cancel also removes the in-flight partial file.
- [x] Verified in **Chromium 151 and Firefox 153** driven over CDP/Marionette:
all four exported formats decode (including the converted FLAC), the
player builds, the gold bar and sprite render, click / space / ←→ /
scrubber-seek / autoplay-next all work, and the network log shows no
request for jquery, audio.min.js or any `.swf`.
- [x] `mix-example/` removed — it was the reference for the template and the
template now reproduces it.
### Round 35 (2026-08-19) — Startup speed: the 21k-track library stops freezing (v0.7.0)
Reported as three separate complaints — "startup takes a minute or two and GNOME