Sync: append mode preserves the playlist image like reconcile (#811)

#811 (reopen of #792, carlosjfcasero, Emby/Jellyfin): "append" mode clobbered
the playlist's custom image. The post-sync image push only excluded
'reconcile' — so append (which edits the playlist in place via
append_to_playlist) still re-pushed the source image over the user's poster
every sync. Now both in-place modes (reconcile + append) skip the image push;
only the destructive 'replace' (recreate-from-scratch) pushes it.

append_to_playlist + set_playlist_image were verified to NOT touch tracks or
description (image push only POSTs /Images/Primary), so this is the identity-
clobber fix for append.

Tests: append + reconcile preserve the image, replace still pushes it.
This commit is contained in:
BoulderBadgeDad 2026-06-07 23:25:42 -07:00
parent 1fba950284
commit 40e3dac881
2 changed files with 49 additions and 6 deletions

View file

@ -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()

View file

@ -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()