playlist_manager.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import json
  2. import os
  3. import logging
  4. import asyncio
  5. from typing import Optional
  6. from modules.core import pattern_manager
  7. from modules.core.state import state
  8. # Configure logging
  9. logger = logging.getLogger(__name__)
  10. # Track the current playlist task so we can cancel it properly
  11. _current_playlist_task: Optional[asyncio.Task] = None
  12. # Global state
  13. PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
  14. # Ensure the file exists and contains at least an empty JSON object
  15. if not os.path.isfile(PLAYLISTS_FILE):
  16. logger.info(f"Creating new playlists file at {PLAYLISTS_FILE}")
  17. with open(PLAYLISTS_FILE, "w") as f:
  18. json.dump({}, f, indent=2)
  19. def load_playlists():
  20. """Load the entire playlists dictionary from the JSON file."""
  21. try:
  22. with open(PLAYLISTS_FILE, "r") as f:
  23. content = f.read().strip()
  24. if not content:
  25. logger.warning("Playlists file is empty, returning empty dict")
  26. return {}
  27. playlists = json.loads(content)
  28. logger.debug(f"Loaded {len(playlists)} playlists")
  29. return playlists
  30. except (json.JSONDecodeError, ValueError) as e:
  31. logger.error(f"Playlists file is corrupted, resetting to empty: {e}")
  32. save_playlists({})
  33. return {}
  34. def save_playlists(playlists_dict):
  35. """Save the entire playlists dictionary back to the JSON file."""
  36. logger.debug(f"Saving {len(playlists_dict)} playlists to file")
  37. with open(PLAYLISTS_FILE, "w") as f:
  38. json.dump(playlists_dict, f, indent=2)
  39. def list_all_playlists():
  40. """Returns a list of all playlist names."""
  41. playlists_dict = load_playlists()
  42. playlist_names = list(playlists_dict.keys())
  43. logger.debug(f"Found {len(playlist_names)} playlists")
  44. return playlist_names
  45. def get_playlist(playlist_name):
  46. """Get a specific playlist by name."""
  47. playlists_dict = load_playlists()
  48. if playlist_name not in playlists_dict:
  49. logger.warning(f"Playlist not found: {playlist_name}")
  50. return None
  51. logger.debug(f"Retrieved playlist: {playlist_name}")
  52. return {
  53. "name": playlist_name,
  54. "files": playlists_dict[playlist_name]
  55. }
  56. def create_playlist(playlist_name, files):
  57. """Create or update a playlist."""
  58. playlists_dict = load_playlists()
  59. playlists_dict[playlist_name] = files
  60. save_playlists(playlists_dict)
  61. logger.info(f"Created/updated playlist '{playlist_name}' with {len(files)} files")
  62. return True
  63. def modify_playlist(playlist_name, files):
  64. """Modify an existing playlist."""
  65. logger.info(f"Modifying playlist '{playlist_name}' with {len(files)} files")
  66. return create_playlist(playlist_name, files)
  67. def delete_playlist(playlist_name):
  68. """Delete a playlist."""
  69. playlists_dict = load_playlists()
  70. if playlist_name not in playlists_dict:
  71. logger.warning(f"Cannot delete non-existent playlist: {playlist_name}")
  72. return False
  73. del playlists_dict[playlist_name]
  74. save_playlists(playlists_dict)
  75. logger.info(f"Deleted playlist: {playlist_name}")
  76. return True
  77. def add_to_playlist(playlist_name, pattern):
  78. """Add a pattern to an existing playlist."""
  79. playlists_dict = load_playlists()
  80. if playlist_name not in playlists_dict:
  81. logger.warning(f"Cannot add to non-existent playlist: {playlist_name}")
  82. return False
  83. playlists_dict[playlist_name].append(pattern)
  84. save_playlists(playlists_dict)
  85. logger.info(f"Added pattern '{pattern}' to playlist '{playlist_name}'")
  86. return True
  87. def rename_playlist(old_name, new_name):
  88. """Rename an existing playlist."""
  89. if not new_name or not new_name.strip():
  90. logger.warning("Cannot rename playlist: new name is empty")
  91. return False, "New name cannot be empty"
  92. new_name = new_name.strip()
  93. playlists_dict = load_playlists()
  94. if old_name not in playlists_dict:
  95. logger.warning(f"Cannot rename non-existent playlist: {old_name}")
  96. return False, "Playlist not found"
  97. if old_name == new_name:
  98. return True, "Name unchanged"
  99. if new_name in playlists_dict:
  100. logger.warning(f"Cannot rename playlist: '{new_name}' already exists")
  101. return False, "A playlist with that name already exists"
  102. # Copy files to new key and delete old key
  103. playlists_dict[new_name] = playlists_dict[old_name]
  104. del playlists_dict[old_name]
  105. save_playlists(playlists_dict)
  106. logger.info(f"Renamed playlist '{old_name}' to '{new_name}'")
  107. return True, f"Playlist renamed to '{new_name}'"
  108. async def cancel_current_playlist():
  109. """Cancel the current playlist task if one is running."""
  110. global _current_playlist_task
  111. if _current_playlist_task and not _current_playlist_task.done():
  112. logger.info("Cancelling existing playlist task...")
  113. _current_playlist_task.cancel()
  114. try:
  115. await _current_playlist_task
  116. except asyncio.CancelledError:
  117. logger.info("Playlist task cancelled successfully")
  118. except Exception as e:
  119. logger.warning(f"Error while cancelling playlist task: {e}")
  120. _current_playlist_task = None
  121. async def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False, runs_per_day=1):
  122. """Run a playlist with the given options.
  123. runs_per_day applies only when run_mode == "scheduled": the playlist will be played
  124. that many full cycles per 24 hours, evenly spread across the day's active hours
  125. (Still Sands periods are excluded from the daily quota).
  126. """
  127. global _current_playlist_task
  128. # Cancel any existing playlist task first
  129. await cancel_current_playlist()
  130. # Also stop any running pattern
  131. if pattern_manager.get_pattern_lock().locked():
  132. logger.info("Another pattern is running, stopping it first...")
  133. await pattern_manager.stop_actions()
  134. playlists = load_playlists()
  135. if playlist_name not in playlists:
  136. logger.error(f"Cannot run non-existent playlist: {playlist_name}")
  137. return False, "Playlist not found"
  138. file_paths = playlists[playlist_name]
  139. file_paths = [os.path.join(pattern_manager.THETA_RHO_DIR, file) for file in file_paths]
  140. if not file_paths:
  141. logger.warning(f"Cannot run empty playlist: {playlist_name}")
  142. return False, "Playlist is empty"
  143. try:
  144. extra = f", runs_per_day={runs_per_day}" if run_mode == "scheduled" else ""
  145. logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}{extra}")
  146. # Set ALL playlist state variables BEFORE creating the async task.
  147. # This ensures state is correct even if the task doesn't start immediately
  148. # (important for TestClient which may cancel background tasks).
  149. state.current_playlist = file_paths
  150. state.current_playlist_name = playlist_name
  151. state.playlist_mode = run_mode
  152. state.current_playlist_index = 0
  153. _current_playlist_task = asyncio.create_task(
  154. pattern_manager.run_theta_rho_files(
  155. file_paths,
  156. pause_time=pause_time,
  157. clear_pattern=clear_pattern,
  158. run_mode=run_mode,
  159. shuffle=shuffle,
  160. runs_per_day=runs_per_day,
  161. )
  162. )
  163. return True, f"Playlist '{playlist_name}' is now running."
  164. except Exception as e:
  165. logger.error(f"Failed to run playlist '{playlist_name}': {str(e)}")
  166. return False, str(e)