diff --git a/core/discovery/sync.py b/core/discovery/sync.py index 82ff0614..aef0acaf 100644 --- a/core/discovery/sync.py +++ b/core/discovery/sync.py @@ -524,12 +524,12 @@ def run_sync_task( # don't want persisted to app.log. _synced = getattr(result, 'synced_tracks', 0) logger.info(f"[PLAYLIST IMAGE] has_image={bool(playlist_image_url)}, synced_tracks={_synced}") - # In reconcile mode the whole point (#792) is to NOT touch the playlist's - # existing image — pushing the source image here would re-clobber a - # user's custom poster every sync, the exact bug reconcile fixes. So skip - # the auto image push for reconcile (the playlist keeps its own art). - if sync_mode == 'reconcile': - logger.info("[PLAYLIST IMAGE] reconcile mode — preserving existing playlist image") + # Modes that edit a playlist in place (reconcile #792, append #811) must + # NOT push the source image — doing so re-clobbers a user's custom poster + # every sync, the exact bug these modes exist to avoid. Only the + # destructive 'replace' (recreate-from-scratch) pushes the image. + if sync_mode in ('reconcile', 'append'): + logger.info(f"[PLAYLIST IMAGE] {sync_mode} mode — preserving existing playlist image") elif playlist_image_url and _synced > 0: try: active_server = deps.config_manager.get_active_media_server() diff --git a/tests/discovery/test_discovery_sync.py b/tests/discovery/test_discovery_sync.py index 2a123804..6e005985 100644 --- a/tests/discovery/test_discovery_sync.py +++ b/tests/discovery/test_discovery_sync.py @@ -364,6 +364,49 @@ def test_playlist_image_uploaded_to_jellyfin(patched_db): assert jf.image_calls == [('PJF', 'https://img/y.png')] +def test_append_mode_preserves_playlist_image(patched_db): + """Append edits in place — it must NOT re-push the source image over the + user's custom poster (#811).""" + jf = _FakeJellyfin() + cfg = _FakeConfig(server='jellyfin') + result = _FakeSyncResult(synced_tracks=3) + svc = _FakeSyncService(media_client=_FakeMediaClient(), sync_result=result) + deps = _build_deps(sync_service=svc, jellyfin=jf, config=cfg) + + ds.run_sync_task('pA', 'PA', [_track()], + playlist_image_url='https://img/a.png', deps=deps, sync_mode='append') + + assert jf.image_calls == [] # preserved, not clobbered + + +def test_reconcile_mode_preserves_playlist_image(patched_db): + """Reconcile likewise preserves the image (#792).""" + jf = _FakeJellyfin() + cfg = _FakeConfig(server='jellyfin') + result = _FakeSyncResult(synced_tracks=3) + svc = _FakeSyncService(media_client=_FakeMediaClient(), sync_result=result) + deps = _build_deps(sync_service=svc, jellyfin=jf, config=cfg) + + ds.run_sync_task('pR', 'PR', [_track()], + playlist_image_url='https://img/r.png', deps=deps, sync_mode='reconcile') + + assert jf.image_calls == [] + + +def test_replace_mode_still_pushes_playlist_image(patched_db): + """Replace recreates from scratch, so it does push the source image.""" + jf = _FakeJellyfin() + cfg = _FakeConfig(server='jellyfin') + result = _FakeSyncResult(synced_tracks=3) + svc = _FakeSyncService(media_client=_FakeMediaClient(), sync_result=result) + deps = _build_deps(sync_service=svc, jellyfin=jf, config=cfg) + + ds.run_sync_task('pRep', 'PRep', [_track()], + playlist_image_url='https://img/rep.png', deps=deps, sync_mode='replace') + + assert jf.image_calls == [('PRep', 'https://img/rep.png')] + + def test_no_image_upload_when_zero_synced(patched_db): """synced_tracks == 0 → no playlist image upload.""" plex = _FakePlex()