| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- # -*- 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})"
- )
|