diff --git a/tests/test_rotation.py b/tests/test_rotation.py index 8216056..6444717 100644 --- a/tests/test_rotation.py +++ b/tests/test_rotation.py @@ -442,3 +442,76 @@ def test_preview_sequence_stores_each_frame(): assert resp_preview.content == expected_bytes finally: _reset_device_context(device_id) + + +def test_playlist_persistence_across_plugin_refresh(): + """Test that user-defined playlists are preserved during plugin refresh.""" + # Setup initial state with a user-defined playlist + master = _prime_rotation_master() + device_id = 'test-persistence' + _reset_device_context(device_id) + + # Set a user-defined playlist (only hashB, not all entries) + user_playlist = ['hashB'] + + # Save to database to simulate existing persistent state + state.persist_default_playlist(user_playlist) + + # Reset rotation master to initial state to simulate fresh server start + _prime_rotation_master() + # Clear selected_ids and has_persistent_playlist to simulate fresh start + state.rotation_master()['selected_ids'] = [] + state.rotation_master()['has_persistent_playlist'] = False + + # Initialize playlists from storage (this should set has_persistent_playlist flag) + state.initialize_rotation_playlists_from_storage() + + # Verify the playlist was loaded and flag was set + assert master['selected_ids'] == user_playlist + assert master.get('has_persistent_playlist') == True + + # Simulate what happens when set_primary_rotation_assets is called during plugin refresh + from trmnl_server.plugins.base import PluginOutput + from trmnl_server.services import state as state_module + from trmnl_server import utils + from io import BytesIO + + # Create mock plugin assets + dummy_path = utils.asset_path('img', 'dummy.bmp') + with open(dummy_path, 'rb') as handle: + bmp_bytes = handle.read() + png_bytes = utils.convert_bmp_bytes_to_png(BytesIO(bmp_bytes)).getvalue() + + # Write temp files for the assets + import tempfile + import os + + with tempfile.TemporaryDirectory() as tmp_dir: + bmp_path = os.path.join(tmp_dir, 'test.bmp') + png_path = os.path.join(tmp_dir, 'test.png') + + with open(bmp_path, 'wb') as f: + f.write(bmp_bytes) + with open(png_path, 'wb') as f: + f.write(png_bytes) + + assets = PluginOutput( + monochrome_path=bmp_path, + grayscale_path=png_path + ) + + try: + # This should NOT overwrite the user playlist since it already exists and has_persistent_playlist flag is set + state_module.set_primary_rotation_assets('TestPlugin', assets) + + # Verify the user playlist is preserved + assert master['selected_ids'] == user_playlist, f"Expected {user_playlist}, got {master['selected_ids']}" + assert len(master['meta']) > 0, "Meta entries should be populated" + + # Verify the playlist wasn't auto-filled with all available entries + available_ids = [entry.get('id') for entry in master['meta'] if entry.get('id')] + assert len(available_ids) > len(user_playlist), "Should have multiple available entries" + assert master['selected_ids'] != available_ids, "Should not auto-fill with all available entries" + + finally: + _reset_device_context(device_id) diff --git a/trmnl_server/services/state.py b/trmnl_server/services/state.py index b4a95de..fa51879 100644 --- a/trmnl_server/services/state.py +++ b/trmnl_server/services/state.py @@ -187,7 +187,8 @@ global_state: Dict[str, Any] = { 'hashes': [], 'meta': [], 'selected_ids': [], - 'version': 0 + 'version': 0, + 'has_persistent_playlist': False }, 'devices': {}, 'device_playlists': {}, @@ -413,6 +414,7 @@ def initialize_rotation_playlists_from_storage() -> None: with STATE_LOCK: master = rotation_master() master['selected_ids'] = default_playlist + master['has_persistent_playlist'] = True for device_id, selected_ids in models.list_device_playlists(): cache_device_playlist(device_id, selected_ids) @@ -688,6 +690,7 @@ def set_default_playlist(playlist_ids: List[str]) -> None: with STATE_LOCK: master = rotation_master() master['selected_ids'] = playlist_ids + master['has_persistent_playlist'] = False # Reset flag when user explicitly sets playlist master['version'] += 1 selection_snapshot = list(master['selected_ids']) persist_default_playlist(selection_snapshot) @@ -916,7 +919,9 @@ def set_primary_rotation_assets( if not had_entries: master['version'] += 1 - if (not master.get('selected_ids')) or auto_fill_enabled: + # Only auto-fill if there's no existing playlist AND no persistent playlist was loaded + # This prevents overwriting user-defined playlists during plugin refresh + if not master.get('selected_ids') and not master.get('has_persistent_playlist'): master['selected_ids'] = [entry.get('id') for entry in meta_list if entry.get('id')] selection_snapshot = list(master['selected_ids']) @@ -991,7 +996,9 @@ def append_rotation_assets( ) ) master['version'] += 1 - if (not master.get('selected_ids')) or auto_fill_enabled: + # Only auto-fill if there's no existing playlist AND no persistent playlist was loaded + # This prevents overwriting user-defined playlists during plugin refresh + if not master.get('selected_ids') and not master.get('has_persistent_playlist'): master['selected_ids'] = [entry.get('id') for entry in meta_list if entry.get('id')] selection_snapshot = list(master['selected_ids'])