from plexapi.server import PlexServer from plexapi.library import LibrarySection, MusicSection from plexapi.audio import Track as PlexTrack, Album as PlexAlbum, Artist as PlexArtist from plexapi.playlist import Playlist as PlexPlaylist from plexapi.exceptions import PlexApiException, NotFound from typing import List, Optional, Dict, Any from dataclasses import dataclass import requests from utils.logging_config import get_logger from config.settings import config_manager logger = get_logger("plex_client") @dataclass class PlexTrackInfo: id: str title: str artist: str album: str duration: int track_number: Optional[int] = None year: Optional[int] = None rating: Optional[float] = None @classmethod def from_plex_track(cls, track: PlexTrack) -> 'PlexTrackInfo': return cls( id=str(track.ratingKey), title=track.title, artist=track.artist().title if track.artist() else "Unknown Artist", album=track.album().title if track.album() else "Unknown Album", duration=track.duration, track_number=track.trackNumber, year=track.year, rating=track.userRating ) @dataclass class PlexPlaylistInfo: id: str title: str description: Optional[str] duration: int leaf_count: int tracks: List[PlexTrackInfo] @classmethod def from_plex_playlist(cls, playlist: PlexPlaylist) -> 'PlexPlaylistInfo': tracks = [] for item in playlist.items(): if isinstance(item, PlexTrack): tracks.append(PlexTrackInfo.from_plex_track(item)) return cls( id=str(playlist.ratingKey), title=playlist.title, description=playlist.summary, duration=playlist.duration, leaf_count=playlist.leafCount, tracks=tracks ) class PlexClient: def __init__(self): self.server: Optional[PlexServer] = None self.music_library: Optional[MusicSection] = None self._setup_client() def _setup_client(self): config = config_manager.get_plex_config() if not config.get('base_url'): logger.warning("Plex server URL not configured") return try: if config.get('token'): self.server = PlexServer(config['base_url'], config['token']) else: logger.error("Plex token not configured") return self._find_music_library() logger.info(f"Successfully connected to Plex server: {self.server.friendlyName}") except Exception as e: logger.error(f"Failed to connect to Plex server: {e}") self.server = None def _find_music_library(self): if not self.server: return try: for section in self.server.library.sections(): if section.type == 'artist': self.music_library = section logger.info(f"Found music library: {section.title}") break if not self.music_library: logger.warning("No music library found on Plex server") except Exception as e: logger.error(f"Error finding music library: {e}") def is_connected(self) -> bool: return self.server is not None and self.music_library is not None def get_all_playlists(self) -> List[PlexPlaylistInfo]: if not self.is_connected(): logger.error("Not connected to Plex server") return [] playlists = [] try: for playlist in self.server.playlists(): if playlist.playlistType == 'audio': playlist_info = PlexPlaylistInfo.from_plex_playlist(playlist) playlists.append(playlist_info) logger.info(f"Retrieved {len(playlists)} audio playlists") return playlists except Exception as e: logger.error(f"Error fetching playlists: {e}") return [] def get_playlist_by_name(self, name: str) -> Optional[PlexPlaylistInfo]: if not self.is_connected(): return None try: playlist = self.server.playlist(name) if playlist.playlistType == 'audio': return PlexPlaylistInfo.from_plex_playlist(playlist) return None except NotFound: logger.info(f"Playlist '{name}' not found") return None except Exception as e: logger.error(f"Error fetching playlist '{name}': {e}") return None def create_playlist(self, name: str, tracks: List[PlexTrackInfo]) -> bool: if not self.is_connected(): logger.error("Not connected to Plex server") return False try: plex_tracks = [] for track_info in tracks: plex_track = self._find_track(track_info.title, track_info.artist, track_info.album) if plex_track: plex_tracks.append(plex_track) else: logger.warning(f"Track not found in Plex: {track_info.title} by {track_info.artist}") if plex_tracks: playlist = self.server.createPlaylist(name, plex_tracks) logger.info(f"Created playlist '{name}' with {len(plex_tracks)} tracks") return True else: logger.error(f"No tracks found for playlist '{name}'") return False except Exception as e: logger.error(f"Error creating playlist '{name}': {e}") return False def update_playlist(self, playlist_name: str, tracks: List[PlexTrackInfo]) -> bool: if not self.is_connected(): return False try: existing_playlist = self.server.playlist(playlist_name) existing_playlist.delete() return self.create_playlist(playlist_name, tracks) except NotFound: logger.info(f"Playlist '{playlist_name}' not found, creating new one") return self.create_playlist(playlist_name, tracks) except Exception as e: logger.error(f"Error updating playlist '{playlist_name}': {e}") return False def _find_track(self, title: str, artist: str, album: str) -> Optional[PlexTrack]: if not self.music_library: return None try: search_results = self.music_library.search(title=title, artist=artist, album=album) for result in search_results: if isinstance(result, PlexTrack): if (result.title.lower() == title.lower() and result.artist().title.lower() == artist.lower() and result.album().title.lower() == album.lower()): return result broader_search = self.music_library.search(title=title, artist=artist) for result in broader_search: if isinstance(result, PlexTrack): if (result.title.lower() == title.lower() and result.artist().title.lower() == artist.lower()): return result return None except Exception as e: logger.error(f"Error searching for track '{title}' by '{artist}': {e}") return None def search_tracks(self, query: str, limit: int = 20) -> List[PlexTrackInfo]: if not self.music_library: return [] try: results = self.music_library.search(query, limit=limit) tracks = [] for result in results: if isinstance(result, PlexTrack): tracks.append(PlexTrackInfo.from_plex_track(result)) return tracks except Exception as e: logger.error(f"Error searching tracks: {e}") return [] def get_library_stats(self) -> Dict[str, int]: if not self.music_library: return {} try: return { 'artists': len(self.music_library.searchArtists()), 'albums': len(self.music_library.searchAlbums()), 'tracks': len(self.music_library.searchTracks()) } except Exception as e: logger.error(f"Error getting library stats: {e}") return {} def update_track_metadata(self, track_id: str, metadata: Dict[str, Any]) -> bool: if not self.is_connected(): return False try: track = self.server.fetchItem(int(track_id)) if isinstance(track, PlexTrack): edits = {} if 'title' in metadata: edits['title'] = metadata['title'] if 'artist' in metadata: edits['artist'] = metadata['artist'] if 'album' in metadata: edits['album'] = metadata['album'] if 'year' in metadata: edits['year'] = metadata['year'] if edits: track.edit(**edits) logger.info(f"Updated metadata for track: {track.title}") return True return False except Exception as e: logger.error(f"Error updating track metadata: {e}") return False