Repair the tags on files already recorded: search, a detail pane, and every field the recorder writes - #46
Merged
Merged
Conversation
There was no way to correct a bad match. The three fields are editable, so a wrong title could be typed over - and doing that forfeits the year, the genre and the artwork that come with a real match, which is most of why anyone opens this page. Re-fetch could not help, and the reason is structural rather than a bug: the automatic lookup builds its query from the file's own artist and title, then discards any result whose artist disagrees with them. Both halves are right. Spotify always returns something, and the top hit for a misparsed filename is routinely a different song, so the gate is what stops the page writing a confident wrong answer into a correctly named file. But it means the one case the automatic path can never reach is a file whose artist is the thing that is wrong, and pressing Re-fetch asks the identical question and gets the identical answer. Auto-fetch skips the row too, because all three fields are filled in. So there is a second lookup with a deliberately different shape: the user's words instead of the file's, several results instead of a verdict, and nothing filtered on the way out. A person reading a list can tell a live take from a studio one, and a filter that could hide the right answer is worse here than a list with a few wrong ones in it. Each result carries its release year, because a search for a song recorded twice returns the original and the remaster with identical titles by identical artists, and the year is the only thing on the row that separates them. The chosen candidate is looked up again rather than tagged from the search result. A search result is not a full track - the release date lives on the album and the genre on the artist - and a hand-picked match arriving thinner than an automatic one would be a strange thing to have built. Both paths share one enrichment step for the same reason: the rule about putting back what Spotify does not offer is the kind that goes wrong quietly, and it would drift the moment there were two copies. Picking a result also drops the file's own cover art, which is the one place the rule that an embedded picture beats a provider's URL is inverted. That rule protects artwork while a lookup confirms what a file already says; choosing a different song says the opposite, and keeping the old sleeve would write correct tags and the previous artist's cover into the same file. That decision then exposed the display bug underneath it. The thumbnail was bound to decoded bytes, but a lookup normally returns a URL and no bytes - the writer downloads it when saving - so it showed the artwork the file already had while claiming to show what saving would do. On a hand-picked match it put the replaced track's sleeve on both sides of the before-and-after. It now binds to either an image or a URI and lets WPF fetch the second, which also keeps the network out of the view model and off whatever thread a fetch happened to finish on. Last.fm is not offered here. Its lookup answers a question about a named artist and title, so it has no results for someone who does not yet know what the track is called, and an empty box that can never return anything would be worse than saying it needs Spotify. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The panel that opens under a track was laid out as a form when what it shows is a comparison. Labels hung in a right-aligned gutter of the page's own invention, one field per line, eight rows deep — a shape no other page in the application uses, so it read as a foreign object, and it pushed the answer to "is this match right?" off the bottom of the list. It is now the recessed well already used elsewhere for "here is what was found", with the label above its field the way every other page writes one, the four fields paired into two columns, and what the match found beside them instead of in a section of its own. Capping the panel's width turned out to be the interesting part. A MaxWidth on a panel aligned Left arranges it at its *desired* width, and a Grid's desired width is what its columns' content asked for — so every box shrank to the word inside it, a 130px box for "Thank Me" beside a 270px one for "Able Heart, Qveen Herby". The cap is now a star column carrying MaxWidth beside an empty Auto one, which is the idiom the rest of the file already uses. Underneath that, three defaults were wrong. The list scrolled by item, so a wheel notch moved three rows and a row here is a three-line block — most of a screen at a time; pixel scrolling moves 48 DIP instead, which was measured rather than assumed. A row had a selected state that nothing on the page acts on, drawn brightly over rows that were neither ticked nor open; removing it takes a template of a bare ContentPresenter, because the themed one paints an accent bar the brush setters do not reach. Focus still moves, and the focus rectangle still shows where it is. And the search box was seeded from the toggle command, which only the row's title runs, so opening a row by its chevron — the control that looks like the way to do it — left the box empty and searching from there asked Spotify for nothing. The last one is a fourth member of the genre false-positive family, and the earlier fix had been made in the wrong place. Putting the file's own genre and year back when the lookup has none was done inside SpotifyCatalogEnricher, which is no help when the chain falls through to Last.fm — its EnrichAsync assigns the artist's tags just as unconditionally and returns nothing for an artist nobody has tagged. The rule now sits at the call site every library lookup passes through, so it holds for whichever provider answers and for any provider added later. Nothing was ever written wrongly — the writer skips empty values — but the row claimed a change Save would not make, and the rebuilt panel spelled it out as an offer to erase a curated tag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row editor was rebuilt twice and tightened once, and each version was judged by looking at it. Measuring it ended the argument. At the shell's minimum window an expanded row is 539 DIP tall and the list viewport it opens inside is 347 — the editor is 1.55x the height of its own container, so opening one file buried the library it was chosen from and pushed the search results off the bottom edge. That is not a spacing problem and no amount of trimming reaches it. The page owns about 130 DIP of the chrome above the list; the rest is the shell's title bar and nav strip. Spending all of it still leaves a deficit, and the best case is a list showing exactly one file. The cheaper variant — moving the search into a popover — lands an opened row at 347 DIP, which is precisely the viewport and the same failure at one decimal place. So the editor is a pane to the right of the list. The list holds five rows at the minimum window and eleven maximized, the pane is the same size and in the same place whatever is picked, and changing the pick reflows nothing: measured before and after a search that returned ten candidates, the list viewport read 373 DIP both times. Three things followed from the split rather than from taste. The selection highlight comes back. Removing it was right for the old page, where nothing acted on the selected row and a highlight tracking the arrow keys competed with the tick box and the chevron for meaning. Here the selection is what the pane shows, so the row has to say which one it is — a tinted background and a narrow accent edge, not the themed container's saturated fill. It also means ApplyFilter has to capture the pick and put it back, because clearing VisibleTracks makes the list null its own SelectedItem, and without that the editor emptied on every keystroke in the filter box, including the keystrokes narrowing the list towards the row being edited. The pane has two regions, not one scroller. Letting the whole thing scroll put ten results below the fold, where the only evidence the search had worked was the scroll bar getting shorter. The fields scroll; the search box and its results are pinned to the foot of the pane, because the results are what the user just asked for and the fields are what they will scroll back to. Seeding the search box moved from the row's expand command to the page's selection, which is the seam every route now crosses, and the intro paragraph became a tooltip on the heading — the same trade the Advanced page makes, for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page shipped with three editable fields — title, artist, album — and showed year, genre and artwork read-only beside them. The scope question "which fields belong here" has an answer that is not a matter of taste: whatever the recording path writes. FFmpegArguments.MetadataArguments writes title, artist, album artist, album, genre, date, track, disc and copyright, so a recording could carry a wrong disc number that no part of Offstream could correct. Composer, comment and BPM are in neither list, which is what keeps this from drifting into a general-purpose tag editor. Filling the gap turned up three defects underneath it, none of them visible while the fields were absent. Ten fields do not stack. 10 x 65 DIP is 650 in a 347 DIP pane — the same arithmetic that moved the editor out of the list row yesterday, met again a day later on the pane that fixed it. Paired into two columns it is 390 DIP: seven of ten visible at the minimum window and all ten at any real size. Numeric boxes read fine at half width, which is what makes the pairing free. An emptied box was reported as a change. HasChanges compared before against after, so clearing the album box lit "Will change" and then saved nothing, because the writer has never written a blank over a value. The predicate now asks whether saving would alter the file, which is the question the badge was always claiming to answer. Writing one artist back over a tag that held two destroyed data. ID3v2.3 separates artists with a slash, so the file recorded as AC/DC holds the two values AC and DC; Read took FirstPerformer, the box showed AC, and Write did tag.Performers = [track.Artist] — narrowing the tag on the page whose purpose is repairing tags. It had been there since the writer was written and stayed invisible until an album artist box appeared beside the artist box, showing "AC, DC" next to "AC". The original list now goes back verbatim when its first entry still matches the box, since that is where the box was filled from; a match satisfies this by construction, because the mapper sets the artist from the first performer it assigns. The tempting fix there is worse than the bug: splitting the box on commas the way the genre box splits reads "Earth, Wind & Fire" as three artists. Genres can be edited as a comma-separated line because a genre list really is a list; names cannot, because commas inside one name are ordinary. Both artist boxes keep the array they were filled from while the text still matches it, and take the whole line as a single value when it does not. The never-erase rule from yesterday became one helper, LibraryLookup, rather than two hand-written copies, and grew from genre and year to all seven fields a lookup can leave empty. There are still two call sites and both are still load-bearing, but the rule itself now exists once, so the next field cannot be added to one copy and forgotten in the other. That is exactly how the bug survived its first fix. Verified on a copy of a real recording rather than the library: a page save with one unrelated field edited left Performers as AC || DC, applied the edit, and kept the file at ID3v2.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #45, which GitHub closed when its base branch
metadata-managerwas deleted onmerging #44; a closed pull request cannot be retargeted, so this is the same branch, rebased
onto
mainand reopened. The four commits and their diff are unchanged.Four commits, and the last three are consequences of the first. Correcting a bad match needed a
search; the search needed somewhere to put its results; and giving the editor that room made it
obvious how few of a file's tags the page could actually reach.
Correcting a match the automatic path cannot reach
There was no way to fix a wrong match. The three fields were editable, so a wrong title could be
typed over — and doing that forfeits the year, the genre and the artwork that come with a real
match, which is most of why anyone opens this page.
Re-fetch could not help, and the reason is structural rather than a bug. The automatic lookup
builds its query from the file's own artist and title, then discards any result whose artist
disagrees with them. Both halves are right: Spotify always returns something, and the top hit for
a misparsed filename is routinely a different song, so the gate is what stops the page writing a
confident wrong answer into a correctly named file. But it means the one case the automatic path
can never reach is a file whose artist is the thing that's wrong — pressing Re-fetch asks the
identical question and gets the identical answer, and Auto-Fetch skips the row entirely because all
three fields are filled in.
Not the right track? is a second lookup with a deliberately different shape from the first: the
user's words instead of the file's, several results instead of a verdict, and nothing filtered on
the way out. A person reading a list can tell a live take from a studio one, and a filter that
could hide the right answer is worse here than a list with a few wrong ones in it. The box is
seeded from the row as it stands, results carry the year so a remaster can be told from the
original, and Use this fills the row in. Nothing reaches a file until Save.
Three decisions there worth the review time:
/tracks/{id}rather than tagged from the searchresult. A search result is not a full track — the release date lives on the album and the genre
on the artist — and a hand-picked match arriving thinner than an automatic one would be a strange
thing to have built. Both paths share one enrichment step (
SpotifyCatalogEnricher).a provider's URL" rule is inverted. That rule protects artwork while a lookup confirms what a
file already says; choosing a different song says the opposite, and keeping the old sleeve would
write correct tags and the previous artist's cover into the same file.
track.getInfoanswers a question about a named artist andtitle — a lookup, not a search — so it has nothing to offer someone who doesn't yet know what the
track is called. An empty box that can never return anything is worse than saying it needs Spotify.
/tracks/{id}is public catalogue data and costs no user scope, soDefaultScopesstays at two andSpotifyAuthOptionsTestsis untouched.CLAUDE.md's call list is updated to six.The editor moved out of the list row, because it never fitted inside one
The row editor was rebuilt twice and tightened once, and every version was judged by looking at it.
Measuring it settled the question in one reading. At the shell's minimum window the track list
viewport is 347 DIP and one expanded row was 539 — an editor 1.55× the height of the
container it opens in. The page owns only ~130 DIP of the chrome above the list, so spending all of
it still leaves a deficit, and the costed alternative (search in a popover) lands the row at exactly
347, which is the same failure at one decimal place.
So the editor is a pane beside the list. The list holds 5 rows at the minimum window and 11
maximized, the pane is the same size and in the same place whatever is picked, and choosing a
different track reflows nothing — measured at 373 DIP before and after a search returning ten
candidates.
The general rule is worth more than the fix: an expander is usable only when the thing it opens is
smaller than the space it opens into. That is a measurement available before the layout is
written, and it is the check two rebuilds skipped.
Three second-order notes:
following the arrow keys while nothing acted on the selected row was noise. In a split view the
selection is the pane's input, so the row has to say which one is showing — a tinted background
and a narrow accent edge, not the themed container's saturated fill.
SelectedItem.ApplyFilterclears and refillsVisibleTracks, and the clear travels out through the two-way binding, so without capturing thepick and restoring it the editor emptied on every keystroke in the filter box — including the
keystrokes narrowing the list toward the row being edited.
the fold, where the only evidence the search had worked was the scroll bar getting shorter. The
fields scroll; the search box and its results are pinned to the foot.
Every tag the recorder writes is now editable
The page shipped with three editable fields and showed year, genre and artwork read-only beside
them. "Which fields belong here" has an answer that is not a matter of taste: whatever the
recording path writes.
FFmpegArguments.MetadataArgumentscovers title, artist, album artist,album, genre, date, track, disc and copyright — so a recording could carry a wrong disc number that
no part of Offstream could correct. Composer, comment and BPM are in neither list, which is what
keeps this from drifting into a general-purpose tag editor.
Ten fields do not stack: 10 × 65 DIP is 650 in a 347 DIP pane, the same arithmetic as the section
above, met a day later on the pane that fixed it. Paired into two columns it is 390 — seven of ten
visible at the minimum window, all ten at any real size. Numeric boxes read fine at half width,
which is what makes the pairing free.
Filling the gap turned up three defects underneath it, none visible while the fields were absent:
with a slash, so a file recorded as
AC/DCholds the two valuesACandDC;ReadtookFirstPerformer, the box showedAC, andWritedidtag.Performers = [track.Artist]—narrowing the tag on the page whose purpose is repairing tags. Pre-existing since the writer was
written, and invisible until an album artist box appeared beside the artist box showing
AC, DCnext to
AC. The original list now goes back verbatim while its first entry still matches thebox, since that is where the box was filled from; a match satisfies this by construction, because
the mapper sets the artist from
performers[0]— now pinned by a test, as the whole fix leans onit.
box splits reads
Earth, Wind & Fireas three artists. Genres can be edited as a comma-separatedline because a genre list really is a list; names cannot, because commas inside one name are
ordinary.
HasChangescompared before againstafter, so clearing the album box lit Will change and then saved nothing — the writer has never
written a blank over a value. The predicate now asks whether saving would alter the file, which is
the question the badge was always claiming to answer.
The never-erase rule became one helper,
LibraryLookup, rather than two hand-written copies, andgrew from genre and year to all seven fields a lookup can leave empty. Still two call sites and both
still load-bearing, but the rule exists once — which matters, because the original bug survived its
first fix precisely by existing twice.
A display bug the artwork decision exposed
The thumbnail was bound to decoded bytes — but a lookup normally returns a URL and no bytes, and
the writer downloads it at save time. So it showed the artwork the file already had while claiming
to show what saving would do, and on a hand-picked match it put the replaced track's sleeve on
both sides of the before-and-after. It now binds to either an image or a
Uriand lets WPF'sImage.Sourcefetch the second, which also keeps the network out of the view model and off whateverthread a fetch happened to finish on.
Every time on this page, the thing that made a bug visible was drawing the value rather than testing
it.
Verification
.\build.ps1 -Clean -Test— 1,269 passing (262 UI + 1,007 Core), 0 warnings.-VerifyFormatclean. Resource keys 210/210 across en and fr.
Driven in the running app against a real library, because that is where every bug on this page has
come from. The search was exercised on a row tagged
AC — Who Made Whoagainstdaft punk one more time, an artist the automatic path structurally cannot reach: applying the result rewrote title,artist and album with a
was …line under each, moved the year, and replaced the sleeve on both therow and the after side. Layout was measured by UIAutomation at the minimum window and maximized
rather than eyeballed.
The save path was verified on a copy of a real recording rather than the library, since Save is
a write to the user's own files: with one unrelated field edited, the file came back with
PerformersstillAC || DC, the edit applied, and the tag still at ID3v2.3. Before the artistfix, that same run left
AC.🤖 Generated with Claude Code