fix: cut long names from the middle, not the end
Build and publish container / build (pull_request) Canceled after 1m18s
Build and publish container / build (pull_request) Canceled after 1m18s
The shortening fitted the path and destroyed its meaning. Lidarr writes "Artist - Album - 07 - Flamethrower.mp3" inside a directory already named for that artist and album, so a long album title occurs three times in one path and everything that distinguishes one track from another sits at the very end. Cutting from the end removed precisely that: King Gizzard & the Lizard Wizard - PetroDragonic Apocalypse; or, Dawn of Eter~c526.mp3 All seven tracks on that record reduced to the same string bar the hash. The path fitted; the result was seven files nobody could tell apart on the device, which is a worse outcome than the failure it replaced. Cut from the middle instead, giving two thirds of the remaining room to the tail because the head is generally a restatement of the directory the file already sits in: King Gizzard & the Lizard~c526~ginning of Merciless Damnation - 07 - Flamethrower.mp3 The eight real paths that prompted this are now regression tests: every track on that album keeps its number and title, all seven names stay distinct, and The Beatles' "The Long One" -- whose length is the title itself rather than a repeated album name -- keeps both ends.
This commit is contained in:
@@ -246,10 +246,26 @@ from `--max-path` to get what a mirror-relative path may spend.
|
||||
Over-budget paths are shortened from the **deepest component outward**: the
|
||||
track name carries the least navigational value and the artist directory the
|
||||
most, so the filename goes first and the artist is touched only if nothing else
|
||||
will do. A shortened component keeps its extension and gains four hex digits of
|
||||
the original name — two long names sharing a prefix would otherwise cut to the
|
||||
same string, and a silent collision between two tracks is worse than an ugly
|
||||
filename.
|
||||
will do.
|
||||
|
||||
A component is cut **from the middle**, not the end, because of how these names
|
||||
are built. Lidarr writes `Artist - Album - 07 - Flamethrower.mp3` inside a
|
||||
directory already named for that artist and album, so a long album title
|
||||
appears three times in one path and the informative part — the track number and
|
||||
title — is at the very end. Cutting from the end throws exactly that away:
|
||||
|
||||
```
|
||||
before King Gizzard & the Lizard Wizard - PetroDragonic Apocalypse; or, Dawn of Eternal
|
||||
Night - An Annihilation of Planet Earth and the Beginning of Merciless
|
||||
Damnation - 07 - Flamethrower.mp3
|
||||
after King Gizzard & the Lizard~c526~ginning of Merciless Damnation - 07 - Flamethrower.mp3
|
||||
```
|
||||
|
||||
Two thirds of the remaining room goes to the tail, since the head is usually a
|
||||
restatement of the directory it sits in. A shortened component gains four hex
|
||||
digits of the original name: two names sharing both a head and a tail would
|
||||
otherwise produce the same string, and a silent collision between two tracks is
|
||||
worse than an ugly filename.
|
||||
|
||||
The result is stable: the same source always produces the same shortened name,
|
||||
so a pass does not rename what the previous pass wrote. A path too deeply
|
||||
|
||||
+21
-6
@@ -168,20 +168,35 @@ def fat32_safe(component):
|
||||
|
||||
|
||||
def shorten_component(component, budget):
|
||||
"""Return a component of at most `budget` characters, marked as shortened.
|
||||
"""Return a component of at most `budget` characters, cut from the middle.
|
||||
|
||||
The mark is four hex digits of the original name. Two different long names
|
||||
would otherwise cut down to the same string, and a silent collision between
|
||||
two tracks is worse than an ugly filename.
|
||||
From the middle, not the end, because of how these names are built. Lidarr
|
||||
writes "Artist - Album - 07 - Flamethrower.mp3" inside a directory already
|
||||
named for that artist and album, so the informative part -- the track
|
||||
number and title -- is at the very end. Cutting from the end discards it
|
||||
and leaves every track on the record with the same name.
|
||||
|
||||
The four hex digits are of the original component. Two names sharing both a
|
||||
head and a tail would otherwise produce the same string, and a silent
|
||||
collision between two tracks is worse than an ugly filename.
|
||||
"""
|
||||
stem, dot, extension = component.rpartition(".")
|
||||
if not dot or len(extension) > 4:
|
||||
stem, extension = component, ""
|
||||
else:
|
||||
extension = dot + extension
|
||||
|
||||
digest = hashlib.blake2s(component.encode("utf-8"), digest_size=2).hexdigest()
|
||||
tail = f"~{digest}{extension}"
|
||||
return stem[: max(1, budget - len(tail))].rstrip(". ") + tail
|
||||
marker = f"~{digest}~"
|
||||
room = max(2, budget - len(extension) - len(marker))
|
||||
if room >= len(stem):
|
||||
return stem + extension
|
||||
|
||||
# Two thirds to the tail: the head is usually a restatement of the
|
||||
# directory it sits in, and the tail is what tells two tracks apart.
|
||||
keep_end = min(len(stem), room * 2 // 3)
|
||||
keep_start = max(1, room - keep_end)
|
||||
return stem[:keep_start].rstrip(". ") + marker + stem[len(stem) - keep_end :] + extension
|
||||
|
||||
|
||||
def fit_path(relative, budget):
|
||||
|
||||
@@ -705,3 +705,62 @@ def test_a_path_that_cannot_be_made_to_fit_is_reported(tmp_path, make_flac, capl
|
||||
run(source, mirror, "--fat32-safe", "--max-path", "80")
|
||||
|
||||
assert "too" in caplog.text and "nested" in caplog.text
|
||||
|
||||
|
||||
# Lidarr writes "Artist - Album - NN - Title.mp3" inside a directory already
|
||||
# named for that artist and album, so a long album title appears three times in
|
||||
# one path. These are real paths from a real library.
|
||||
GIZZARD_ALBUM = (
|
||||
"PetroDragonic Apocalypse; or, Dawn of Eternal Night - An Annihilation of"
|
||||
" Planet Earth and the Beginning of Merciless Damnation"
|
||||
)
|
||||
GIZZARD_TRACKS = (
|
||||
"01 - Motor Spirit",
|
||||
"02 - Supercell",
|
||||
"03 - Converge",
|
||||
"04 - Witchcraft",
|
||||
"05 - Gila Monster",
|
||||
"06 - Dragon",
|
||||
"07 - Flamethrower",
|
||||
)
|
||||
|
||||
|
||||
def gizzard_path(track):
|
||||
return Path(
|
||||
"King Gizzard & the Lizard Wizard",
|
||||
f"{GIZZARD_ALBUM} (2023)",
|
||||
f"King Gizzard & the Lizard Wizard - {GIZZARD_ALBUM} - {track}.mp3",
|
||||
)
|
||||
|
||||
|
||||
def test_shortening_keeps_the_part_that_tells_tracks_apart():
|
||||
"""Cutting from the end discards the track number and title, which is all
|
||||
that distinguishes one track on the record from another."""
|
||||
for track in GIZZARD_TRACKS:
|
||||
fitted = music_mirror.fit_path(gizzard_path(track), 253)
|
||||
assert len(str(fitted)) <= 253
|
||||
assert fitted.name.endswith(f"{track}.mp3"), fitted.name
|
||||
|
||||
|
||||
def test_every_track_on_a_long_album_keeps_a_distinct_name():
|
||||
fitted = {music_mirror.fit_path(gizzard_path(t), 253).name for t in GIZZARD_TRACKS}
|
||||
|
||||
assert len(fitted) == len(GIZZARD_TRACKS)
|
||||
|
||||
|
||||
def test_a_long_title_keeps_both_ends():
|
||||
"""The Beatles' 'The Long One' is one track whose title is the long part."""
|
||||
path = Path(
|
||||
"The Beatles",
|
||||
"Abbey Road (1969)",
|
||||
"Digital Media 03",
|
||||
"The Beatles - Abbey Road - 09 - The Long One - You Never Give Me Your Money"
|
||||
" + Sun King + Mean Mr Mustard + Her Majesty + Polythene Pam + She Came In"
|
||||
" Through the Bathroom Window+ Golden Slumbers + Carry That Weight + The End.mp3",
|
||||
)
|
||||
|
||||
fitted = music_mirror.fit_path(path, 253)
|
||||
|
||||
assert len(str(fitted)) <= 253
|
||||
assert fitted.name.startswith("The Beatles - Abbey Road - 09 - The Long One")
|
||||
assert fitted.name.endswith("The End.mp3")
|
||||
|
||||
Reference in New Issue
Block a user