track.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. # -*- coding: utf-8 -*-
  2. """
  3. Track-Datenklassen.
  4. Repräsentiert einen einzelnen Musik-Track mit Metadaten.
  5. """
  6. from dataclasses import dataclass, field
  7. from datetime import datetime, timedelta
  8. from enum import Enum
  9. from pathlib import Path
  10. from typing import Any
  11. class TrackState(Enum):
  12. """Zustand eines Tracks."""
  13. UNKNOWN = "unknown" # Unbekannt
  14. AVAILABLE = "available" # Verfügbar zum Abspielen
  15. LOADING = "loading" # Wird geladen
  16. PLAYING = "playing" # Wird abgespielt
  17. PAUSED = "paused" # Pausiert
  18. BUFFERING = "buffering" # Wird gepuffert
  19. ERROR = "error" # Fehler
  20. UNAVAILABLE = "unavailable" # Nicht verfügbar
  21. @dataclass
  22. class TrackMetadata:
  23. """
  24. Metadaten eines Tracks.
  25. Enthält ID3-Tags und weitere Informationen.
  26. """
  27. # Basis-Informationen
  28. title: str = ""
  29. artist: str = ""
  30. album: str = ""
  31. album_artist: str = ""
  32. # Track-Nummern
  33. track_number: int = 0
  34. track_total: int = 0
  35. disc_number: int = 1
  36. disc_total: int = 1
  37. # Zeitinformationen
  38. duration_ms: int = 0
  39. year: int = 0
  40. # Genre und Stimmung
  41. genre: str = ""
  42. mood: str = ""
  43. # Artwork
  44. artwork_url: str = ""
  45. artwork_data: bytes = field(default=b"", repr=False)
  46. # Technische Informationen
  47. sample_rate: int = 44100
  48. bit_rate: int = 0
  49. channels: int = 2
  50. format: str = ""
  51. # Zusätzliche Tags
  52. composer: str = ""
  53. lyrics: str = ""
  54. comment: str = ""
  55. # Benutzerdefiniert
  56. custom: dict[str, Any] = field(default_factory=dict)
  57. @property
  58. def duration(self) -> timedelta:
  59. """Dauer als timedelta."""
  60. return timedelta(milliseconds=self.duration_ms)
  61. @property
  62. def duration_formatted(self) -> str:
  63. """Dauer als formatierter String (MM:SS)."""
  64. total_seconds = self.duration_ms // 1000
  65. minutes = total_seconds // 60
  66. seconds = total_seconds % 60
  67. return f"{minutes}:{seconds:02d}"
  68. @property
  69. def display_artist(self) -> str:
  70. """Artist für Anzeige (Album-Artist falls vorhanden)."""
  71. return self.album_artist or self.artist or "Unbekannter Künstler"
  72. @property
  73. def display_title(self) -> str:
  74. """Titel für Anzeige."""
  75. return self.title or "Unbekannter Titel"
  76. def to_dict(self) -> dict[str, Any]:
  77. """Konvertiert zu Dictionary (ohne binäre Daten)."""
  78. return {
  79. "title": self.title,
  80. "artist": self.artist,
  81. "album": self.album,
  82. "album_artist": self.album_artist,
  83. "track_number": self.track_number,
  84. "disc_number": self.disc_number,
  85. "duration_ms": self.duration_ms,
  86. "duration_formatted": self.duration_formatted,
  87. "year": self.year,
  88. "genre": self.genre,
  89. "artwork_url": self.artwork_url,
  90. "sample_rate": self.sample_rate,
  91. "bit_rate": self.bit_rate,
  92. "channels": self.channels,
  93. "format": self.format,
  94. }
  95. @dataclass
  96. class Track:
  97. """
  98. Repräsentiert einen Musik-Track.
  99. Kann von verschiedenen Quellen stammen (lokal, Spotify, etc.).
  100. """
  101. # Identifikation
  102. id: str = ""
  103. uri: str = "" # Eindeutiger Identifier (file://, spotify:, etc.)
  104. # Quelle
  105. source_type: str = "" # "local", "spotify", "amazon", "url", "usb"
  106. source_id: str = "" # ID innerhalb der Quelle
  107. # Pfad (für lokale Dateien)
  108. path: Path | None = None
  109. # Metadaten
  110. metadata: TrackMetadata = field(default_factory=TrackMetadata)
  111. # Zustand
  112. state: TrackState = TrackState.UNKNOWN
  113. error_message: str = ""
  114. # Caching
  115. cached: bool = False
  116. cache_path: Path | None = None
  117. # Zeitstempel
  118. added_at: datetime = field(default_factory=datetime.now)
  119. last_played: datetime | None = None
  120. play_count: int = 0
  121. # Zusätzliche Daten
  122. extra: dict[str, Any] = field(default_factory=dict)
  123. def __post_init__(self) -> None:
  124. """Generiert ID falls nicht vorhanden."""
  125. if not self.id:
  126. import uuid
  127. self.id = f"track_{uuid.uuid4().hex[:12]}"
  128. if not self.uri:
  129. if self.path:
  130. self.uri = f"file://{self.path}"
  131. elif self.source_type and self.source_id:
  132. self.uri = f"{self.source_type}:{self.source_id}"
  133. @property
  134. def is_available(self) -> bool:
  135. """Prüft ob Track verfügbar ist."""
  136. return self.state in (TrackState.AVAILABLE, TrackState.PLAYING, TrackState.PAUSED)
  137. @property
  138. def is_playing(self) -> bool:
  139. """Prüft ob Track gerade abgespielt wird."""
  140. return self.state == TrackState.PLAYING
  141. @property
  142. def is_local(self) -> bool:
  143. """Prüft ob Track eine lokale Datei ist."""
  144. return self.source_type in ("local", "usb") and self.path is not None
  145. @property
  146. def title(self) -> str:
  147. """Kurzzugriff auf Titel."""
  148. return self.metadata.display_title
  149. @property
  150. def artist(self) -> str:
  151. """Kurzzugriff auf Artist."""
  152. return self.metadata.display_artist
  153. @property
  154. def duration_ms(self) -> int:
  155. """Kurzzugriff auf Dauer."""
  156. return self.metadata.duration_ms
  157. def mark_played(self) -> None:
  158. """Markiert Track als abgespielt."""
  159. self.last_played = datetime.now()
  160. self.play_count += 1
  161. def set_error(self, message: str) -> None:
  162. """Setzt Fehlerzustand."""
  163. self.state = TrackState.ERROR
  164. self.error_message = message
  165. def to_dict(self) -> dict[str, Any]:
  166. """Konvertiert zu Dictionary."""
  167. return {
  168. "id": self.id,
  169. "uri": self.uri,
  170. "source_type": self.source_type,
  171. "source_id": self.source_id,
  172. "path": str(self.path) if self.path else None,
  173. "metadata": self.metadata.to_dict(),
  174. "state": self.state.value,
  175. "cached": self.cached,
  176. "play_count": self.play_count,
  177. "last_played": self.last_played.isoformat() if self.last_played else None,
  178. }
  179. def __repr__(self) -> str:
  180. return (
  181. f"Track(id={self.id!r}, "
  182. f"title={self.title!r}, "
  183. f"artist={self.artist!r}, "
  184. f"source={self.source_type!r})"
  185. )