| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- # -*- coding: utf-8 -*-
- """
- Playlist-Manager.
- Verwaltet mehrere Playlists mit Speicherung und Laden.
- """
- import json
- import logging
- from datetime import datetime
- from pathlib import Path
- from typing import Any
- from trixy_core.music.playlist.playlist import Playlist, PlaylistItem, RepeatMode, ShuffleMode
- from trixy_core.music.track import Track, TrackMetadata
- class PlaylistManager:
- """
- Verwaltet mehrere Playlists.
- Bietet Funktionen zum Erstellen, Speichern, Laden und Suchen von Playlists.
- """
- def __init__(
- self,
- storage_path: Path | None = None,
- auto_save: bool = True,
- ) -> None:
- """
- Initialisiert den PlaylistManager.
- Args:
- storage_path: Pfad für Playlist-Speicherung
- auto_save: Automatisch speichern bei Änderungen
- """
- self.storage_path = storage_path or Path("./playlists")
- self.auto_save = auto_save
- self.logger = logging.getLogger(__name__)
- self._playlists: dict[str, Playlist] = {}
- self._favorites: Playlist | None = None
- # Storage-Verzeichnis erstellen
- if self.storage_path:
- self.storage_path.mkdir(parents=True, exist_ok=True)
- @property
- def count(self) -> int:
- """Anzahl der Playlists."""
- return len(self._playlists)
- @property
- def playlists(self) -> list[Playlist]:
- """Alle Playlists."""
- return list(self._playlists.values())
- @property
- def favorites(self) -> Playlist:
- """Favoriten-Playlist (wird automatisch erstellt)."""
- if not self._favorites:
- self._favorites = self.get("__favorites__")
- if not self._favorites:
- self._favorites = self.create(
- name="Favoriten",
- playlist_id="__favorites__",
- )
- return self._favorites
- # ==========================================================================
- # CRUD-Operationen
- # ==========================================================================
- def create(
- self,
- name: str,
- description: str = "",
- owner: str = "",
- playlist_id: str = "",
- ) -> Playlist:
- """
- Erstellt eine neue Playlist.
- Args:
- name: Name der Playlist
- description: Beschreibung
- owner: Eigentümer
- playlist_id: Optionale ID
- Returns:
- Erstellte Playlist
- """
- playlist = Playlist(
- id=playlist_id,
- name=name,
- description=description,
- owner=owner,
- )
- self._playlists[playlist.id] = playlist
- if self.auto_save:
- self.save_playlist(playlist)
- self.logger.debug(f"Playlist erstellt: {playlist.name} ({playlist.id})")
- return playlist
- def get(self, playlist_id: str) -> Playlist | None:
- """
- Holt Playlist nach ID.
- Args:
- playlist_id: Playlist-ID
- Returns:
- Playlist oder None
- """
- return self._playlists.get(playlist_id)
- def get_by_name(self, name: str) -> list[Playlist]:
- """
- Sucht Playlists nach Name.
- Args:
- name: Suchstring (case-insensitive)
- Returns:
- Liste passender Playlists
- """
- name_lower = name.lower()
- return [p for p in self._playlists.values() if name_lower in p.name.lower()]
- def delete(self, playlist_id: str) -> bool:
- """
- Löscht eine Playlist.
- Args:
- playlist_id: Playlist-ID
- Returns:
- True wenn gelöscht
- """
- if playlist_id in self._playlists:
- playlist = self._playlists.pop(playlist_id)
- # Datei löschen
- if self.storage_path:
- file_path = self.storage_path / f"{playlist_id}.json"
- if file_path.exists():
- file_path.unlink()
- self.logger.debug(f"Playlist gelöscht: {playlist.name}")
- return True
- return False
- def rename(self, playlist_id: str, new_name: str) -> bool:
- """
- Benennt eine Playlist um.
- Args:
- playlist_id: Playlist-ID
- new_name: Neuer Name
- Returns:
- True wenn erfolgreich
- """
- playlist = self.get(playlist_id)
- if playlist:
- playlist.name = new_name
- playlist.modified_at = datetime.now()
- if self.auto_save:
- self.save_playlist(playlist)
- return True
- return False
- def duplicate(self, playlist_id: str, new_name: str = "") -> Playlist | None:
- """
- Dupliziert eine Playlist.
- Args:
- playlist_id: ID der zu duplizierenden Playlist
- new_name: Name der Kopie
- Returns:
- Duplizierte Playlist oder None
- """
- original = self.get(playlist_id)
- if not original:
- return None
- copy = self.create(
- name=new_name or f"{original.name} (Kopie)",
- description=original.description,
- owner=original.owner,
- )
- # Tracks kopieren
- for item in original.items:
- copy.add(item.track, added_by=item.added_by)
- return copy
- # ==========================================================================
- # Speicherung
- # ==========================================================================
- def save_playlist(self, playlist: Playlist) -> bool:
- """
- Speichert eine Playlist.
- Args:
- playlist: Zu speichernde Playlist
- Returns:
- True wenn erfolgreich
- """
- if not self.storage_path:
- return False
- file_path = self.storage_path / f"{playlist.id}.json"
- try:
- data = self._playlist_to_dict(playlist)
- with open(file_path, "w", encoding="utf-8") as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
- return True
- except Exception as e:
- self.logger.error(f"Fehler beim Speichern: {e}")
- return False
- def load_playlist(self, playlist_id: str) -> Playlist | None:
- """
- Lädt eine Playlist aus Datei.
- Args:
- playlist_id: Playlist-ID
- Returns:
- Geladene Playlist oder None
- """
- if not self.storage_path:
- return None
- file_path = self.storage_path / f"{playlist_id}.json"
- if not file_path.exists():
- return None
- try:
- with open(file_path, "r", encoding="utf-8") as f:
- data = json.load(f)
- playlist = self._dict_to_playlist(data)
- self._playlists[playlist.id] = playlist
- return playlist
- except Exception as e:
- self.logger.error(f"Fehler beim Laden: {e}")
- return None
- def load_all(self) -> int:
- """
- Lädt alle Playlists aus dem Storage-Verzeichnis.
- Returns:
- Anzahl der geladenen Playlists
- """
- if not self.storage_path or not self.storage_path.exists():
- return 0
- count = 0
- for file_path in self.storage_path.glob("*.json"):
- playlist_id = file_path.stem
- if self.load_playlist(playlist_id):
- count += 1
- self.logger.info(f"{count} Playlists geladen")
- return count
- def save_all(self) -> int:
- """
- Speichert alle Playlists.
- Returns:
- Anzahl der gespeicherten Playlists
- """
- count = 0
- for playlist in self._playlists.values():
- if self.save_playlist(playlist):
- count += 1
- return count
- # ==========================================================================
- # Serialisierung
- # ==========================================================================
- def _playlist_to_dict(self, playlist: Playlist) -> dict[str, Any]:
- """Konvertiert Playlist zu Dictionary für Speicherung."""
- return {
- "id": playlist.id,
- "name": playlist.name,
- "description": playlist.description,
- "owner": playlist.owner,
- "is_public": playlist.is_public,
- "repeat_mode": playlist.repeat_mode.value,
- "shuffle_mode": playlist.shuffle_mode.value,
- "artwork_url": playlist.artwork_url,
- "created_at": playlist.created_at.isoformat(),
- "modified_at": playlist.modified_at.isoformat(),
- "metadata": playlist.metadata,
- "items": [
- {
- "track": {
- "id": item.track.id,
- "uri": item.track.uri,
- "source_type": item.track.source_type,
- "source_id": item.track.source_id,
- "path": str(item.track.path) if item.track.path else None,
- "metadata": {
- "title": item.track.metadata.title,
- "artist": item.track.metadata.artist,
- "album": item.track.metadata.album,
- "duration_ms": item.track.metadata.duration_ms,
- "artwork_url": item.track.metadata.artwork_url,
- },
- },
- "position": item.position,
- "added_at": item.added_at.isoformat(),
- "added_by": item.added_by,
- }
- for item in playlist.items
- ],
- }
- def _dict_to_playlist(self, data: dict[str, Any]) -> Playlist:
- """Konvertiert Dictionary zu Playlist."""
- playlist = Playlist(
- id=data["id"],
- name=data["name"],
- description=data.get("description", ""),
- owner=data.get("owner", ""),
- is_public=data.get("is_public", False),
- repeat_mode=RepeatMode(data.get("repeat_mode", "off")),
- shuffle_mode=ShuffleMode(data.get("shuffle_mode", "off")),
- artwork_url=data.get("artwork_url", ""),
- metadata=data.get("metadata", {}),
- )
- # Zeitstempel
- if "created_at" in data:
- playlist.created_at = datetime.fromisoformat(data["created_at"])
- if "modified_at" in data:
- playlist.modified_at = datetime.fromisoformat(data["modified_at"])
- # Items laden
- for item_data in data.get("items", []):
- track_data = item_data["track"]
- metadata = TrackMetadata(
- title=track_data.get("metadata", {}).get("title", ""),
- artist=track_data.get("metadata", {}).get("artist", ""),
- album=track_data.get("metadata", {}).get("album", ""),
- duration_ms=track_data.get("metadata", {}).get("duration_ms", 0),
- artwork_url=track_data.get("metadata", {}).get("artwork_url", ""),
- )
- track = Track(
- id=track_data.get("id", ""),
- uri=track_data.get("uri", ""),
- source_type=track_data.get("source_type", ""),
- source_id=track_data.get("source_id", ""),
- path=Path(track_data["path"]) if track_data.get("path") else None,
- metadata=metadata,
- )
- item = PlaylistItem(
- track=track,
- position=item_data.get("position", 0),
- added_by=item_data.get("added_by", ""),
- )
- if "added_at" in item_data:
- item.added_at = datetime.fromisoformat(item_data["added_at"])
- playlist.items.append(item)
- return playlist
- # ==========================================================================
- # Hilfsmethoden
- # ==========================================================================
- def search(self, query: str) -> list[tuple[Playlist, list[Track]]]:
- """
- Sucht in allen Playlists nach Tracks.
- Args:
- query: Suchstring
- Returns:
- Liste von (Playlist, passende Tracks)
- """
- query_lower = query.lower()
- results = []
- for playlist in self._playlists.values():
- matching_tracks = []
- for item in playlist.items:
- if (query_lower in item.track.title.lower() or
- query_lower in item.track.artist.lower() or
- query_lower in item.track.metadata.album.lower()):
- matching_tracks.append(item.track)
- if matching_tracks:
- results.append((playlist, matching_tracks))
- return results
- def get_recent(self, limit: int = 10) -> list[Playlist]:
- """
- Holt kürzlich geänderte Playlists.
- Args:
- limit: Maximale Anzahl
- Returns:
- Liste der Playlists (neueste zuerst)
- """
- sorted_playlists = sorted(
- self._playlists.values(),
- key=lambda p: p.modified_at,
- reverse=True,
- )
- return sorted_playlists[:limit]
- def __contains__(self, playlist_id: str) -> bool:
- return playlist_id in self._playlists
- def __getitem__(self, playlist_id: str) -> Playlist:
- return self._playlists[playlist_id]
- def __iter__(self):
- return iter(self._playlists.values())
- def __len__(self) -> int:
- return len(self._playlists)
|