v0.11.1: a web mix stops shipping an .m3u

It was there on the theory that it cost nothing and let the folder double as a
plain music folder. But a web mix is a folder you upload, and a stray playlist
file next to index.html is one more thing to explain to whoever receives it.

plan.m3u_name is now "" for WEB, so the plan doesn't name a file it won't
write, and index.html is simply the last thing written — which is what the
manifest-last invariant always meant for that branch. Folder exports keep
their m3u unchanged.

Claude-Session: https://claude.ai/code/session_01JBSM2bFC6UToiEg8BE4dqj
This commit is contained in:
2026-08-22 15:22:01 -04:00
parent ce7fe8abed
commit ce8b0f26d9
5 changed files with 37 additions and 18 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux."""
__version__ = "0.11.0"
__version__ = "0.11.1"
+15 -10
View File
@@ -11,8 +11,9 @@ Two invariants worth keeping:
* **Local music files are only ever read.** Like sync, this never moves,
rewrites or deletes anything in the library.
* **The manifest is written last** — ``index.html`` for a web mix, the
``.m3u`` for a folder. An interrupted export therefore never leaves behind
a playlist promising files that aren't there.
``.m3u`` for a folder (a web mix gets no m3u: it is a page to upload, not a
music folder to open in a player). An interrupted export therefore never
leaves behind a page or playlist promising files that aren't there.
"""
import html
@@ -103,7 +104,7 @@ class ExportPlan:
playlist_name: str
dest_dir: Path # the folder that gets created
audio_dir: Path # dest_dir, or dest_dir/"audios" for a web mix
m3u_name: str
m3u_name: str # "" for a web mix, which writes no m3u
title: str = ""
description: str = ""
image: Path | None = None # None = ship the gray placeholder
@@ -135,7 +136,7 @@ def plan_export(playlist_name: str, tracks: list, dest_parent: Path,
playlist_name=playlist_name,
dest_dir=dest_dir,
audio_dir=dest_dir / "audios" if kind == WEB else dest_dir,
m3u_name=folder + ".m3u",
m3u_name="" if kind == WEB else folder + ".m3u",
title=playlist_name,
description=DEFAULT_DESCRIPTION,
)
@@ -369,14 +370,18 @@ class ExportWorker(QObject):
self.failed.emit(f"Export failed: {e}")
def _write_manifest(self, completed: list):
"""The .m3u, plus the page and its assets for a web mix. Written last."""
"""The page for a web mix, the .m3u for a folder. Written last.
A web mix gets no m3u. It used to ship one on the theory that it cost
nothing and let the folder double as a plain music folder — but the
folder is a page you upload, and a stray playlist file next to
index.html is one more thing to explain to whoever receives it.
"""
plan = self._plan
entries = [(item.dest_name, item.secs, item.display) for item in completed]
if plan.kind == WEB:
for name in _WEB_ASSETS:
shutil.copyfile(templates() / name, plan.dest_dir / name)
image_name = None
if plan.image is not None and Path(plan.image).is_file():
image_name = sanitize_name(Path(plan.image).stem) \
+ Path(plan.image).suffix.lower()
@@ -385,11 +390,11 @@ class ExportWorker(QObject):
image_name = "placeholder.png"
shutil.copyfile(templates() / "placeholder.png",
plan.dest_dir / image_name)
# An m3u alongside the page costs nothing and lets the folder
# double as a plain music folder.
entries = [("audios/" + n, s, d) for n, s, d in entries]
(plan.dest_dir / "index.html").write_text(
build_index_html(plan, image_name), encoding="utf-8")
return
entries = [(item.dest_name, item.secs, item.display)
for item in completed]
(plan.dest_dir / plan.m3u_name).write_text(
build_m3u(entries), encoding="utf-8")