Skip to content

Add "Hide duplicate songs" option to Music Management - #2877

Open
ImanMontajabi wants to merge 3 commits into
PixelPlayerHQ:masterfrom
ImanMontajabi:feature/hide-duplicate-songs
Open

ImanMontajabi wants to merge 3 commits into
PixelPlayerHQ:masterfrom
ImanMontajabi:feature/hide-duplicate-songs

Conversation

@ImanMontajabi

Copy link
Copy Markdown

Summary

  • Adds a Hide duplicate songs switch under Settings > Music Management > Filtering (off by default).
  • When enabled, the cloud sync skips songs that are already available from a higher-priority source (local > Telegram > Netease > Navidrome), so the same track is no longer listed twice. This happens, for example, when songs downloaded from a Telegram channel are also streamed from that channel after connecting it.
  • Matching uses normalized title (case, punctuation, diacritics, Arabic/Persian letter variants), duration within 2 seconds and a compatible artist. Untagged files whose title is a file name such as Song.mp3 ignore the extension and the artist, since the artist is unreliable there.
  • Toggling the option triggers a full sync, so the library updates right away. Turning it off restores the hidden songs.
  • Duplicate files within the same cloud source are reduced to one copy, using a stable order so unified IDs don't change between syncs.

Notes

  • Only the English string is added; other languages fall back to English.
  • Local-vs-local duplicates and Google Drive, QQ Music and Jellyfin songs are not filtered.
  • Playlists created for a Telegram channel keep the IDs of hidden copies, which simply don't resolve.

Test plan

  • DuplicateSongMatcherTest unit tests (10 cases) pass
  • ./gradlew :app:assembleDebug succeeds
  • Emulator: with 2 local songs plus seeded Telegram and Netease songs, the library showed 7 entries with the option off and 4 with it on; turning it off again restored all 7
  • Tested on a real device with a Telegram channel whose songs were already downloaded locally
  • Navidrome path not exercised on a device

Made with Cursor

Cloud songs (Telegram, Netease, Navidrome) were shown next to the same track already on the device, e.g. after adding a Telegram channel whose songs were downloaded earlier. With the new option enabled, the cloud sync skips songs that match a higher-priority source (local > Telegram > Netease > Navidrome) by normalized title, duration and artist, and ignores file extensions and untagged artists.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 01:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unbounded artist substring matching can hide legitimate songs by different artists.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds configurable cross-source song deduplication using normalized metadata and source priority.

Changes:

  • Adds the setting and persisted preference.
  • Filters Telegram, Netease, and Navidrome sync results.
  • Adds duplicate-matching tests.
File Description
DuplicateSongMatcherTest.kt Tests matching behavior.
strings_settings.xml Adds setting labels.
DuplicateSongMatcher.kt Implements duplicate detection.
SettingsViewModel.kt Exposes setting and triggers resync.
SettingsCategoryScreen.kt Adds the filtering switch.
SyncWorker.kt Filters Telegram and Netease songs.
UserPreferencesRepository.kt Persists the preference.
NavidromeRepository.kt Filters Navidrome songs.
MusicDao.kt Loads higher-priority song fingerprints.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +77 to +80
if (a == b || a.contains(b) || b.contains(a)) return true
// e.g. title "Alaki (BLH Remix)" with artist tag "BLH Remix".
return titleKey.isNotEmpty() &&
(titleKey.contains(a) || titleKey.contains(b))
Substring checks let artists such as "Queen" and "Queens of the Stone Age" match, hiding legitimate songs. Artists are now split into credits and must share a full credit; the title fallback only considers bracketed segments like "(BLH Remix)".

Co-authored-by: Cursor <cursoragent@cursor.com>
@ImanMontajabi

Copy link
Copy Markdown
Author

Addressed the Copilot review in b77dbb3. Artists are now compared by whole credits (split on feat./ft./x/&/,/etc.) and must share at least one full credit, so "Queen" no longer matches "Queens of the Stone Age". The title fallback only looks at bracketed segments such as "(BLH Remix)" instead of arbitrary containment. Added tests for both cases (14 matcher tests pass).

The hide-duplicates setting now uses one filter for every cloud source, including the Netease path that could reinsert songs after SyncWorker hid them. Hidden copies move their favorite flag and playlist entries to the song that stays, and matching requires a second signal besides title so unknown artists without duration no longer collide.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ImanMontajabi

Copy link
Copy Markdown
Author

Follow-up cleanup in f0137c8 after a full review of the first implementation:

  • One CloudDuplicateFilter now covers Telegram, Netease, Google Drive, QQ Music, Navidrome, and Jellyfin, including the Netease repository path that could put hidden songs back.
  • Favorites and playlist entries on a hidden copy are moved to the song that stays, before the hidden row is deleted.
  • Matching now needs a second signal besides title: unknown artists without a usable duration no longer match.
  • 16 matcher tests and 3 playlist-id rewrite tests pass; :app:compileDebugKotlin succeeds.

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.

2 participants