Repository navigation
Finding the clean copy the library does not hold #9
robert-dean
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Written: 2026-08-16, from building the advisory policy.
State of the tree: the policy is built and shipped.
track_sources.advisoryholdsexplicit/clean/ null,rotation.advisoryisprefer-explicit/prefer-clean/clean-only, andCandidatesRepository.bindingsForchooses between the copies the catalog alreadyhas. What does not exist is anything that goes LOOKING for a copy the catalog does not have.
What was built, and exactly where it stops
A clean edit and the explicit original are one
deadair.tracksrow with twotrack_sourcesbindings, because
resolveTrackmatches ontitle_key+ artist and the clean edit's own ISRCmisses. So the whole policy is binding selection, and it is honest as far as it goes:
prefer-cleantakes the clean binding if the library already holds one.clean-onlyrefuses the work outright if it does not.The gap is the "if". The library is filled by walking the connected account's playlists, so it holds
whichever copy those playlists happened to carry — and a playlist of explicit originals produces a
library where
prefer-cleanleans on nothing andclean-onlyrefuses records whose clean versionthe provider is perfectly willing to serve. That is the same shape as the gap
ProviderTrackLookupwas built to close for a record the library had never heard of at all.
Why this is a separate mechanism and not a flag on the existing lookup
ProviderTrackLookupis STRICT on purpose, and its own file says why: a near-miss does not raise anerror, it airs the wrong record. Both the normalized title and the normalized lead artist must match
exactly, and duration only breaks a tie between candidates that already matched.
A clean-copy search needs the exact opposite of that on one axis and MORE than it on another:
(Clean),[Clean],(Radio Edit),(Edited),(Clean Version), and combinations with a feature creditalready in the title. Matching on
normalizeKeyalone finds nothing in exactly the cases thisexists for.
find a live version, a remix, a cover, or a different song on the same album. The thing being
matched is a RECORDING, not a name.
Putting both behaviours behind a flag on one class would give the strict path a mode in which it is
not strict, which is the property the whole ingest path rests on. So: a separate class, and the
existing one untouched.
The shape it would take
Where it drops in.
PickResolver, afteridentifyand before the rules — the same slotdiscoveroccupies, and for the same reason: a copy taken in has to become a real binding, becausethe player fetches every record through
track_sourcesand a copy with no binding has no URL. Itruns only when the policy is
prefer-cleanorclean-onlyAND the work's existing bindings are allexplicit or unmarked. Under
prefer-explicitit is inert.The matcher. Search the provider for the artist plus the base title, then score candidates:
(Clean),(Radio Edit),(Edited),(Clean Version),(Album Version)), then require an exactnormalizeKeymatch on what is left.normalizeKey, as everything else in this codebasedoes — see pick-artist-matching for what a credit line in that field costs.
advisory: 'clean'. This is the whole point: a candidate thatdoes not positively say is not a clean copy, it is an unmarked one, and taking it would smuggle
the "silence is consent" reading in through the back door after the policy deliberately refused
it.
ProviderTrackLookupdoes. A cleanedit that is thirty seconds shorter is a radio edit as well, which is a different recording and a
different question — see below.
The cache is the reason to build it at all. The pairing between an explicit track id and its
clean counterpart is PERMANENT: it is a fact about two rows in a provider's catalog and it does not
change. So the search must happen once per record ever, not once per refill, and the result has to
be stored — including the NEGATIVE result, since "this record has no clean version" is the common
answer and re-searching for it every hour would spend a provider's whole rate budget on records that
will never have one. A miss needs to be distinguishable from "never looked", the way
fact_extractionsexists so a pass can tell an article that yielded nothing from one it neveropened.
Two things deliberately out of scope
A radio edit is not a clean edit. They are correlated and they are not the same axis: an edit is
a LENGTH cut and may still be explicit. The
advisorycolumn is about lyrics alone, and anedit-length preference — a station that wants the 3:30 version rather than the 7:00 album cut —
belongs in its own field on the day something wants it. Do not widen this one to carry it, and do
not let the matcher's
(Radio Edit)stripping quietly turn into an edit-length preference.The account-level filter is still not enforced. See below.
The measurement this was deferred with
An operator's own Spotify account can have explicit content turned off, and that decision sits above
the station's. The plugin now READS it —
explicit_content.filter_enabledandfilter_lockedoffGET /me, on a profile call it was already making — and reports it intestConnection's message andonce in its log. It does not act on it, and the reason is that nobody has measured whether it binds:
the station's audio comes through the shim rather than the Web API, so whether that filter actually
refuses the bytes is an open question and asserting either answer in code would be a guess.
Settle it before building any of the above, because the answer changes what this doc is worth. On
an account with the filter on, ask
TrackAudioService.ensurefor a binding markedadvisory = 'explicit'and watch whether the bytes arrive.matcher above is the whole of the remaining work.
is already a column for exactly that:
track_sources.playable, documented as "the provider stillknows the track but will not serve it here". That would make a clean-copy matcher considerably
more valuable, because a filtered account currently degrades into four failed fetches and a benched
binding with nothing in the log connecting it to a checkbox.
Write the answer back into this file either way. The question is cheap to settle once and expensive
to keep re-deriving.
Moved from
docs/todo/clean-copy-matching.md. The file as it last stood: https://github.com/robert-dean/deadair/blob/71e431d45ce7b35a189c7d226832ec4a6247e9dc/docs/todo/clean-copy-matching.mdAll reactions