import os import logging import requests from typing import Dict, Any, Optional, List from pathlib import Path logger = logging.getLogger(__name__) class PlaylistManager: """Manages playlists and media server integration.""" def __init__(self, config): """Initialize the playlist manager. Args: config: Configuration service instance """ self.config = config self.jellyfin_address = config.jellyfin_address.rstrip('/') if config.jellyfin_address else '' self.jellyfin_api_key = config.jellyfin_api_key def trigger_media_refresh(self, paths: List[str] = None) -> bool: """Trigger a media library refresh on the configured media server. Args: paths: Optional list of paths to refresh Returns: bool: True if refresh was triggered successfully, False otherwise """ if self.config.trigger_jellyfin_scan and self.jellyfin_address and self.jellyfin_api_key: return self._trigger_jellyfin_refresh(paths) return False def _trigger_jellyfin_refresh(self, paths: List[str] = None) -> bool: """Trigger a Jellyfin library refresh. Args: paths: Optional list of paths to refresh Returns: bool: True if refresh was triggered successfully, False otherwise """ try: headers = { 'X-Emby-Token': self.jellyfin_api_key, 'Content-Type': 'application/json' } # If specific paths are provided, refresh only those if paths: data = { 'Updates': [ {'Path': path, 'UpdateType': 'Created'} for path in paths ] } url = f"{self.jellyfin_address}/Library/Media/Updated" else: # Full library scan url = f"{self.jellyfin_address}/Library/Refresh" data = {} response = requests.post(url, headers=headers, json=data, timeout=30) response.raise_for_status() logger.info("Jellyfin library refresh triggered successfully") return True except Exception as e: logger.error(f"Failed to trigger Jellyfin refresh: {e}") return False def generate_playlist(self, name: str, tracks: List[Dict[str, Any]]) -> str: """Generate an M3U playlist file. Args: name: Name of the playlist tracks: List of track dictionaries with at least 'path' key Returns: str: Path to the generated playlist file """ if not self.config.generate_m3u_playlist: return "" try: # Create playlist directory if it doesn't exist playlist_dir = Path(self.config.m3u_playlist_path or '/data/playlists') playlist_dir.mkdir(parents=True, exist_ok=True) # Create playlist file playlist_path = playlist_dir / f"{name}.m3u" with open(playlist_path, 'w', encoding='utf-8') as f: # Write M3U header f.write("#EXTM3U\n") # Write each track for track in tracks: track_path = track.get('path', '') if not track_path: continue # Convert to relative path if needed if track_path.startswith(self.config.absolute_server_path): track_path = track_path[len(self.config.absolute_server_path):].lstrip('/') # Write extended info f.write(f"#EXTINF:{int(track.get('duration', 0))},{track.get('artist', 'Unknown')} - {track.get('title', 'Unknown')}\n") f.write(f"{track_path}\n") logger.info(f"Generated playlist: {playlist_path}") return str(playlist_path) except Exception as e: logger.error(f"Failed to generate playlist: {e}") return "" def add_to_playlist(self, playlist_name: str, track_paths: List[str]) -> bool: """Add tracks to a playlist, creating it if it doesn't exist. Args: playlist_name: Name of the playlist track_paths: List of track file paths to add Returns: bool: True if successful, False otherwise """ # This is a simplified implementation that regenerates the entire playlist # A more efficient implementation would read the existing playlist first try: # Create track info for each path tracks = [{'path': path} for path in track_paths] # Generate or update the playlist playlist_path = self.generate_playlist(playlist_name, tracks) # Trigger media server refresh if configured if playlist_path and self.config.trigger_jellyfin_scan: self.trigger_media_refresh([playlist_path]) return bool(playlist_path) except Exception as e: logger.error(f"Failed to add tracks to playlist: {e}") return False def get_playlist_tracks(self, playlist_name: str) -> List[str]: """Get the list of tracks in a playlist. Args: playlist_name: Name of the playlist Returns: List of track file paths """ try: playlist_path = Path(self.config.m3u_playlist_path or '/data/playlists') / f"{playlist_name}.m3u" if not playlist_path.exists(): return [] tracks = [] with open(playlist_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line and not line.startswith('#'): # Convert relative paths to absolute if needed if not os.path.isabs(line): line = os.path.join(self.config.absolute_server_path, line) tracks.append(line) return tracks except Exception as e: logger.error(f"Failed to read playlist: {e}") return []