# -*- coding: utf-8 -*- """ Track-Datenklassen. Repräsentiert einen einzelnen Musik-Track mit Metadaten. """ from dataclasses import dataclass, field from datetime import datetime, timedelta from enum import Enum from pathlib import Path from typing import Any class TrackState(Enum): """Zustand eines Tracks.""" UNKNOWN = "unknown" # Unbekannt AVAILABLE = "available" # Verfügbar zum Abspielen LOADING = "loading" # Wird geladen PLAYING = "playing" # Wird abgespielt PAUSED = "paused" # Pausiert BUFFERING = "buffering" # Wird gepuffert ERROR = "error" # Fehler UNAVAILABLE = "unavailable" # Nicht verfügbar @dataclass class TrackMetadata: """ Metadaten eines Tracks. Enthält ID3-Tags und weitere Informationen. """ # Basis-Informationen title: str = "" artist: str = "" album: str = "" album_artist: str = "" # Track-Nummern track_number: int = 0 track_total: int = 0 disc_number: int = 1 disc_total: int = 1 # Zeitinformationen duration_ms: int = 0 year: int = 0 # Genre und Stimmung genre: str = "" mood: str = "" # Artwork artwork_url: str = "" artwork_data: bytes = field(default=b"", repr=False) # Technische Informationen sample_rate: int = 44100 bit_rate: int = 0 channels: int = 2 format: str = "" # Zusätzliche Tags composer: str = "" lyrics: str = "" comment: str = "" # Benutzerdefiniert custom: dict[str, Any] = field(default_factory=dict) @property def duration(self) -> timedelta: """Dauer als timedelta.""" return timedelta(milliseconds=self.duration_ms) @property def duration_formatted(self) -> str: """Dauer als formatierter String (MM:SS).""" total_seconds = self.duration_ms // 1000 minutes = total_seconds // 60 seconds = total_seconds % 60 return f"{minutes}:{seconds:02d}" @property def display_artist(self) -> str: """Artist für Anzeige (Album-Artist falls vorhanden).""" return self.album_artist or self.artist or "Unbekannter Künstler" @property def display_title(self) -> str: """Titel für Anzeige.""" return self.title or "Unbekannter Titel" def to_dict(self) -> dict[str, Any]: """Konvertiert zu Dictionary (ohne binäre Daten).""" return { "title": self.title, "artist": self.artist, "album": self.album, "album_artist": self.album_artist, "track_number": self.track_number, "disc_number": self.disc_number, "duration_ms": self.duration_ms, "duration_formatted": self.duration_formatted, "year": self.year, "genre": self.genre, "artwork_url": self.artwork_url, "sample_rate": self.sample_rate, "bit_rate": self.bit_rate, "channels": self.channels, "format": self.format, } @dataclass class Track: """ Repräsentiert einen Musik-Track. Kann von verschiedenen Quellen stammen (lokal, Spotify, etc.). """ # Identifikation id: str = "" uri: str = "" # Eindeutiger Identifier (file://, spotify:, etc.) # Quelle source_type: str = "" # "local", "spotify", "amazon", "url", "usb" source_id: str = "" # ID innerhalb der Quelle # Pfad (für lokale Dateien) path: Path | None = None # Metadaten metadata: TrackMetadata = field(default_factory=TrackMetadata) # Zustand state: TrackState = TrackState.UNKNOWN error_message: str = "" # Caching cached: bool = False cache_path: Path | None = None # Zeitstempel added_at: datetime = field(default_factory=datetime.now) last_played: datetime | None = None play_count: int = 0 # Zusätzliche Daten extra: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: """Generiert ID falls nicht vorhanden.""" if not self.id: import uuid self.id = f"track_{uuid.uuid4().hex[:12]}" if not self.uri: if self.path: self.uri = f"file://{self.path}" elif self.source_type and self.source_id: self.uri = f"{self.source_type}:{self.source_id}" @property def is_available(self) -> bool: """Prüft ob Track verfügbar ist.""" return self.state in (TrackState.AVAILABLE, TrackState.PLAYING, TrackState.PAUSED) @property def is_playing(self) -> bool: """Prüft ob Track gerade abgespielt wird.""" return self.state == TrackState.PLAYING @property def is_local(self) -> bool: """Prüft ob Track eine lokale Datei ist.""" return self.source_type in ("local", "usb") and self.path is not None @property def title(self) -> str: """Kurzzugriff auf Titel.""" return self.metadata.display_title @property def artist(self) -> str: """Kurzzugriff auf Artist.""" return self.metadata.display_artist @property def duration_ms(self) -> int: """Kurzzugriff auf Dauer.""" return self.metadata.duration_ms def mark_played(self) -> None: """Markiert Track als abgespielt.""" self.last_played = datetime.now() self.play_count += 1 def set_error(self, message: str) -> None: """Setzt Fehlerzustand.""" self.state = TrackState.ERROR self.error_message = message def to_dict(self) -> dict[str, Any]: """Konvertiert zu Dictionary.""" return { "id": self.id, "uri": self.uri, "source_type": self.source_type, "source_id": self.source_id, "path": str(self.path) if self.path else None, "metadata": self.metadata.to_dict(), "state": self.state.value, "cached": self.cached, "play_count": self.play_count, "last_played": self.last_played.isoformat() if self.last_played else None, } def __repr__(self) -> str: return ( f"Track(id={self.id!r}, " f"title={self.title!r}, " f"artist={self.artist!r}, " f"source={self.source_type!r})" )