#905 (part 1): push the deduped track list to the media server, not the raw matched list
sync_playlist computed deduped_tracks but the dispatch (append/reconcile/replace) sent the raw valid_tracks — so a library track matched by more than one source entry was pushed multiple times each sync. Extracted _dedupe_by_rating_key (tested) and routed all three modes through it. This fixes the WITHIN-sync duplication. The cross-sync growth reporters describe (Navidrome playlist doubling every resync) is a separate server-push issue still under diagnosis.
This commit is contained in:
parent
9d16abf952
commit
10a1e1337f
2 changed files with 67 additions and 15 deletions
|
|
@ -37,6 +37,23 @@ def _plex_track_file(plex_track) -> str:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_by_rating_key(tracks: list) -> list:
|
||||||
|
"""Drop repeated media-server tracks (same ratingKey), preserving first-seen
|
||||||
|
order. The same library track can match more than one source entry (or appear
|
||||||
|
twice in the source), and pushing the dupes made reconcile/replace re-add the
|
||||||
|
track every sync — part of the #905 doubling. The sync dispatch MUST send this
|
||||||
|
deduped list, not the raw matched list."""
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for t in tracks:
|
||||||
|
key = getattr(t, 'ratingKey', None)
|
||||||
|
if key is None or key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(t)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def reresolve_manual_match_live_plex(cache_db, media_client, m, *, profile_id,
|
def reresolve_manual_match_live_plex(cache_db, media_client, m, *, profile_id,
|
||||||
source_track_id, server_source):
|
source_track_id, server_source):
|
||||||
"""Re-resolve a manual match whose stored Plex ratingKey went stale.
|
"""Re-resolve a manual match whose stored Plex ratingKey went stale.
|
||||||
|
|
@ -438,21 +455,16 @@ class PlaylistSyncService:
|
||||||
f"{server_type.title()} objects with ratingKeys"
|
f"{server_type.title()} objects with ratingKeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Deduplicate by ratingKey — media servers silently drop duplicates
|
# Deduplicate by ratingKey — media servers silently drop duplicates,
|
||||||
seen_keys = set()
|
# and pushing dupes made every sync re-add the same track (#905). The
|
||||||
deduped_tracks = []
|
# dispatch below sends THIS deduped list, never the raw `valid_tracks`.
|
||||||
for t in valid_tracks:
|
plex_tracks = _dedupe_by_rating_key(valid_tracks)
|
||||||
if t.ratingKey not in seen_keys:
|
if len(plex_tracks) < len(valid_tracks):
|
||||||
seen_keys.add(t.ratingKey)
|
|
||||||
deduped_tracks.append(t)
|
|
||||||
if len(deduped_tracks) < len(valid_tracks):
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Deduplicated {len(valid_tracks) - len(deduped_tracks)} duplicate ratingKeys "
|
f"Deduplicated {len(valid_tracks) - len(plex_tracks)} duplicate ratingKeys "
|
||||||
f"({len(valid_tracks)} → {len(deduped_tracks)} tracks)"
|
f"({len(valid_tracks)} → {len(plex_tracks)} tracks)"
|
||||||
)
|
)
|
||||||
|
|
||||||
plex_tracks = deduped_tracks
|
|
||||||
|
|
||||||
if not media_client:
|
if not media_client:
|
||||||
logger.error("No active media client available for playlist sync")
|
logger.error("No active media client available for playlist sync")
|
||||||
sync_success = False
|
sync_success = False
|
||||||
|
|
@ -462,11 +474,11 @@ class PlaylistSyncService:
|
||||||
f"(mode: {sync_mode})"
|
f"(mode: {sync_mode})"
|
||||||
)
|
)
|
||||||
if sync_mode == 'append':
|
if sync_mode == 'append':
|
||||||
sync_success = media_client.append_to_playlist(playlist.name, valid_tracks)
|
sync_success = media_client.append_to_playlist(playlist.name, plex_tracks)
|
||||||
elif sync_mode == 'reconcile':
|
elif sync_mode == 'reconcile':
|
||||||
sync_success = self._reconcile_or_replace(media_client, playlist.name, valid_tracks)
|
sync_success = self._reconcile_or_replace(media_client, playlist.name, plex_tracks)
|
||||||
else:
|
else:
|
||||||
sync_success = media_client.update_playlist(playlist.name, valid_tracks)
|
sync_success = media_client.update_playlist(playlist.name, plex_tracks)
|
||||||
|
|
||||||
synced_tracks = len(plex_tracks) if sync_success else 0
|
synced_tracks = len(plex_tracks) if sync_success else 0
|
||||||
# Not in library (for wishlist), not "total minus playlist size".
|
# Not in library (for wishlist), not "total minus playlist size".
|
||||||
|
|
|
||||||
40
tests/test_sync_dedupe.py
Normal file
40
tests/test_sync_dedupe.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"""Sync must push a ratingKey-deduped track list (#905).
|
||||||
|
|
||||||
|
The dedup was computed but the dispatch sent the raw matched list, so a track
|
||||||
|
matched by more than one source entry got pushed multiple times — and on
|
||||||
|
reconcile/replace that re-added it every sync (playlists doubling). This guards
|
||||||
|
the pure dedup helper the dispatch now uses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from services.sync_service import _dedupe_by_rating_key
|
||||||
|
|
||||||
|
|
||||||
|
class _T:
|
||||||
|
def __init__(self, rk):
|
||||||
|
self.ratingKey = rk
|
||||||
|
|
||||||
|
|
||||||
|
def test_removes_duplicate_rating_keys_preserving_order():
|
||||||
|
a, b, c = _T('1'), _T('2'), _T('3')
|
||||||
|
dup_b = _T('2')
|
||||||
|
out = _dedupe_by_rating_key([a, b, c, dup_b, a])
|
||||||
|
assert [t.ratingKey for t in out] == ['1', '2', '3']
|
||||||
|
assert out[1] is b # first-seen object kept, not the later duplicate
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_duplicates_is_identity():
|
||||||
|
items = [_T('1'), _T('2'), _T('3')]
|
||||||
|
assert _dedupe_by_rating_key(items) == items
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_tracks_without_rating_key():
|
||||||
|
class _NoKey:
|
||||||
|
pass
|
||||||
|
out = _dedupe_by_rating_key([_T('1'), _NoKey(), _T('2')])
|
||||||
|
assert [t.ratingKey for t in out] == ['1', '2']
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty():
|
||||||
|
assert _dedupe_by_rating_key([]) == []
|
||||||
Loading…
Reference in a new issue