Skip to content

feat(tags): Add multi-artist and multi-genre tag splitting - #1028

Open
SteveZMTstudios wants to merge 1 commit into
FoedusProgramme:betafrom
SteveZMTstudios:feat/multi-artist
Open

SteveZMTstudios wants to merge 1 commit into
FoedusProgramme:betafrom
SteveZMTstudios:feat/multi-artist

Conversation

@SteveZMTstudios

Copy link
Copy Markdown
Contributor

Summary

Tracks whose artist or genre tag holds several values (A; B, A & B, A feat. B) currently land in the library as a single artist with that whole string as its name, so they never show up under either artist. This adds opt-in splitting so they are indexed under each value separately.

Both switches default to off, so an existing library is unaffected until the user turns them on and the library is rescanned.

What it does

  • Tag Splitting category in Behavior settings with two switches, Multi-artist support and Multi-genre support, each with its own delimiter page underneath. A delimiter page stays disabled while its switch is off.
  • Delimiter pages (SplitTagSettingsActivity) edit symbols and words as dismissible chips, validate as you type (single character, no letters or digits, no backslash, no whitespace in words, no duplicates), and offer Reset to default from the toolbar.
  • Artist navigation stops guessing. When a track has several artists, tapping the artist opens a picker; otherwise it goes straight to the artist page. Resolution is id → exact name → case-insensitive name, so entries reached by name (Android Auto, media ids like artist_<name>) still land on the right page when the MediaStore id is not available.
  • Search matches title, album, artist and genre through one shared predicate.
  • Play queue persists the new extras and the raw tag, so a restored queue keeps showing what it showed before.

How it works

Tokenizer — TagSplitter is the single place that knows about delimiters. Symbol delimiters match anywhere, word delimiters only when surrounded by whitespace ((?<=\s)(?:feat\.|ft\.)(?=\s)), so feat. inside a name is left alone. A backslash before a delimiter escapes it: escaped delimiters are swapped for placeholders before matching and restored afterwards. Splitting and display formatting happen in one pass, and the display string is normalized onto the primary delimiter so A & B and A; B render the same way.

Artist aggregation — Reader.read now builds artists in two passes. The first pass fills a canonical (trimmed, lowercased) name → MediaStore artist id cache from all songs; the second maps each song onto its artists against that finished cache (MiscUtils.aggregateArtistsInto, a pure function). The order is load-bearing: resolving during the scan would make results depend on which song was read first.

Album artists — MiscUtils.resolveAlbumArtist takes the album-artist tag by majority (60% of tracks, ties broken against null, with a warning logged when there is no clear winner) and otherwise infers the album artist from the track artist tags. Artists without a MediaStore id get a deterministic negative synthetic id derived from the canonical name.

Plumbing — MediaItemExtras exposes rawArtist, artistNames and genreNames on top of EXTRA_RAW_ARTIST, EXTRA_ARTIST_NAMES and EXTRA_GENRE_NAMES; each falls back to the unsplit value, so callers that never see a split item keep working. GramophoneApplication publishes a tagSplitConfigFlow that re-runs the reader, debounced so rapid chip edits do not trigger a rescan storm — but the first emission is not delayed, so cold start is unaffected.

Defaults and trade-offs

  • Default delimiters are ;, , and 、, the separators used by mp3tag, MusicBrainz Picard and most streaming exports.
  • ASCII ,, & and / are not in the default set on purpose: they occur inside plenty of single-entity names (Tyler, The Creator, Earth, Wind & Fire, AC/DC, Above & Beyond, Simon & Garfunkel). Users who want them can add them.
  • A name that would be split can be protected by editing the tag and escaping the character (AC\/DC). This is the only escape hatch and it is a tag edit, not a per-file app setting — that is a deliberate choice, not an oversight.
  • Enabling a delimiter that really does occur inside a name means a fragment can resolve onto a different artist that happens to share that name. The escape above is the intended remedy.

Scope notes

  • Search additionally matches genre now, including with multi-genre off (the fallback is the unsplit genre tag), so the search surface grows slightly on the default path.
  • Artist entries are now keyed on the canonical name with the MediaStore id as fallback, so names that differ only in case or surrounding whitespace merge into one entry.
  • app/src/main/res/layout/preference_basic.xml is a shared layout (~28 references across the settings screens). It now points at two new colour state lists, which is what lets a dependent preference grey out. The selectors' enabled state is byte-identical to the previous values, so already-enabled preferences render exactly as before.
  • isColorOS() gained ro.build.version.realmeui, which keeps StrictMode off on realme UI devices; that behaviour was already intended for ColorOS/OPPO builds.

No new permissions, no manifest components other than the settings activity (exported=false), no database migration, no change to minSdk.

Testing

./gradlew :app:testDebugUnitTest — 24 new tests in TagSplitterTest:

  • splitting on symbols, on words, and on CJK/ASCII mixed input
  • single-entity names (AC/DC, Above & Beyond, Earth, Wind & Fire, …) left intact under the defaults
  • backslash escaping, including words
  • symbol/word normalization and deduplication
  • synthetic ids are strictly negative and stable across casing
  • album-artist majority fallback
  • two-pass aggregation against real MediaStore ids

Checklist

  • New strings added to the default values/strings.xml
  • Feature is opt-in and off by default
  • ./gradlew :app:assembleDebug
  • ./gradlew :app:testDebugUnitTest
  • Verified on a device with a multi-artist library

Screenshots

Artist selector per artist srlit config
Screenshot_2026-09-21-21-13-19-69_7b63ba6ba855e67dd5ee1b08e9300b77 Screenshot_2026-09-21-21-13-49-57_7b63ba6ba855e67dd5ee1b08e9300b77 Screenshot_2026-09-21-21-14-08-15_7b63ba6ba855e67dd5ee1b08e9300b77

Opt-in support for splitting delimited artist and genre tags into separate library entries, with the delimiter lists configurable from a new Tag Splitting section in Behavior settings. Both switches default to off, so nothing changes for existing libraries until the user enables it and the library is rescanned.

Core:
- TagSplitter: one tokenizer shared by artist and genre splitting.
  Symbol delimiters match anywhere, word delimiters only on whitespace boundaries, a leading backslash escapes either, results are trimmed and deduplicated case-insensitively, and the display string is   normalized onto the primary delimiter. Delimiter lists are stored per type as JSON in the default preferences and always fall back to the defaults when empty or invalid.
- Reader: artist aggregation becomes two deterministic passes.
  The first fills a canonical (trimmed, lowercased) name -> MediaStore artist id cache from every song; the second maps each song onto its artists, so the outcome no longer depends on scan order. Album artists are resolved by majority vote with a track-artist inference fallback, and artists without a MediaStore id get stable negative synthetic ids.
- Multi-value tags travel with the item as EXTRA_ARTIST_NAMES,
  EXTRA_GENRE_NAMES and EXTRA_RAW_ARTIST, keeping the raw tag available for display and letting the split values survive queue round-trips.

UI:
- Behavior settings gains a Tag Splitting category: two switches and a delimiter page each, disabled while their switch is off.
- SplitTagSettingsActivity edits symbols and words as dismissible chips, validates input while typing, and can reset to the defaults.
- Tapping the artist of a track that has several artists asks which one to open instead of guessing. Lookup is id -> exact name -> case-insensitive name.

Other:
- Search matches title, album, artist and genre through a single shared predicate, replacing three copies of the same expression.
- LastPlayedManager persists the new extras (queue format v4) and reads the tag-split config from the default preferences.
- Default delimiters are ';', ',' and '、'. ',', '&' and '/' are deliberately not included because they occur inside common single-entity names such as "AC/DC" and "Above & Beyond"; users can add them, and a name can be protected by escaping the character in the tag itself (AC\/DC).
- isColorOS() also matches realme UI, keeping StrictMode off on those devices.
SteveZMTstudios added a commit to SteveZMTstudios/Gramophone that referenced this pull request Sep 22, 2026
…ti-artist

feat(tags): Add multi-artist and multi-genre tag splitting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant