plexbar 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: plexbar
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A command-line TUI music player for Plex
5
5
  Author: Christopher Patti
6
6
  Author-email: Christopher Patti <feoh@feoh.org>
7
7
  Requires-Dist: platformdirs>=4.10.0
8
8
  Requires-Dist: plexapi>=4.18.1
9
9
  Requires-Dist: textual>=8.2.7
10
+ Requires-Dist: textual-image>=0.13.2
10
11
  Requires-Python: >=3.12
11
12
  Description-Content-Type: text/markdown
12
13
 
@@ -24,6 +25,7 @@ opening a web browser.
24
25
  - Browse by Artists, Albums, Tracks, and Playlists
25
26
  - Search your Plex music library
26
27
  - Queue tracks, albums, artists, and playlists
28
+ - Show cover art for the currently playing track or album
27
29
  - Play, pause/resume, stop, and skip with keyboard shortcuts
28
30
  - Local playback through `mpv`
29
31
 
@@ -12,6 +12,7 @@ opening a web browser.
12
12
  - Browse by Artists, Albums, Tracks, and Playlists
13
13
  - Search your Plex music library
14
14
  - Queue tracks, albums, artists, and playlists
15
+ - Show cover art for the currently playing track or album
15
16
  - Play, pause/resume, stop, and skip with keyboard shortcuts
16
17
  - Local playback through `mpv`
17
18
 
@@ -1,11 +1,16 @@
1
1
  [project]
2
2
  name = "plexbar"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "A command-line TUI music player for Plex"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Christopher Patti", email = "feoh@feoh.org" }]
7
7
  requires-python = ">=3.12"
8
- dependencies = ["platformdirs>=4.10.0", "plexapi>=4.18.1", "textual>=8.2.7"]
8
+ dependencies = [
9
+ "platformdirs>=4.10.0",
10
+ "plexapi>=4.18.1",
11
+ "textual>=8.2.7",
12
+ "textual-image>=0.13.2",
13
+ ]
9
14
 
10
15
  [project.scripts]
11
16
  plexbar = "plexbar.app:main"
@@ -15,9 +20,4 @@ requires = ["uv_build>=0.9.18,<0.10.0"]
15
20
  build-backend = "uv_build"
16
21
 
17
22
  [dependency-groups]
18
- dev = [
19
- "mypy>=2.1.0",
20
- "pre-commit>=4.6.0",
21
- "pytest>=9.0.3",
22
- "ruff>=0.15.16",
23
- ]
23
+ dev = ["mypy>=2.1.0", "pre-commit>=4.6.0", "pytest>=9.0.3", "ruff>=0.15.16"]
@@ -1,6 +1,8 @@
1
1
  """Textual application for Plexbar."""
2
2
 
3
3
  from collections.abc import Callable
4
+ from io import BytesIO
5
+ from urllib.request import urlopen
4
6
 
5
7
  from textual import on, work
6
8
  from textual.app import App, ComposeResult
@@ -17,6 +19,7 @@ from textual.widgets import (
17
19
  ListView,
18
20
  Static,
19
21
  )
22
+ from textual_image.widget import AutoImage
20
23
 
21
24
  from plexbar.models import BrowserItem, ItemKind, QueueTrack
22
25
  from plexbar.playback import MpvNotFoundError, MpvPlayer, PlaybackQueue
@@ -164,6 +167,12 @@ class PlexbarApp(App[None]):
164
167
  border-top: solid $primary;
165
168
  }
166
169
 
170
+ #cover-art {
171
+ width: 100%;
172
+ height: 18;
173
+ margin: 0 1 1 1;
174
+ }
175
+
167
176
  .panel-title {
168
177
  text-style: bold;
169
178
  padding: 0 1;
@@ -187,6 +196,7 @@ class PlexbarApp(App[None]):
187
196
  self.client: PlexMusicClient | None = None
188
197
  self.player: MpvPlayer | None = None
189
198
  self.queue = PlaybackQueue()
199
+ self.current_track: QueueTrack | None = None
190
200
  self.history: list[list[BrowserItem]] = []
191
201
  self.items: list[BrowserItem] = []
192
202
 
@@ -199,6 +209,7 @@ class PlexbarApp(App[None]):
199
209
  yield ListView(id="browser-list")
200
210
  with Vertical(id="side"):
201
211
  yield Label("Now Playing", classes="panel-title")
212
+ yield AutoImage(id="cover-art")
202
213
  yield Static("Nothing playing", id="now-playing")
203
214
  yield Label("Queue", classes="panel-title")
204
215
  yield Static("Queue is empty", id="queue")
@@ -207,6 +218,7 @@ class PlexbarApp(App[None]):
207
218
 
208
219
  def on_mount(self) -> None:
209
220
  self.query_one("#search", Input).display = False
221
+ self.query_one("#cover-art", AutoImage).display = False
210
222
  self.set_interval(0.5, self.advance_finished_track)
211
223
  if config_exists():
212
224
  try:
@@ -359,6 +371,7 @@ class PlexbarApp(App[None]):
359
371
 
360
372
  track = self.queue.next()
361
373
  if track is None:
374
+ self.clear_now_playing()
362
375
  self.set_status(end_status)
363
376
  self.refresh_queue()
364
377
  return
@@ -368,6 +381,7 @@ class PlexbarApp(App[None]):
368
381
  def action_stop(self) -> None:
369
382
  if self.player is not None:
370
383
  self.player.stop()
384
+ self.clear_now_playing()
371
385
  self.set_status("Stopped.")
372
386
 
373
387
  def append_item(self, item: BrowserItem) -> None:
@@ -385,10 +399,46 @@ class PlexbarApp(App[None]):
385
399
  if self.player is None:
386
400
  self.set_status("mpv is not available.")
387
401
  return
402
+ self.current_track = track
388
403
  self.player.play(track)
389
404
  self.query_one("#now-playing", Static).update(track.label)
405
+ self.show_cover_art(track)
390
406
  self.set_status(f"Playing {track.label}")
391
407
 
408
+ def show_cover_art(self, track: QueueTrack) -> None:
409
+ """Load and display the current track's Plex artwork."""
410
+
411
+ cover_art = self.query_one("#cover-art", AutoImage)
412
+ cover_art.image = None
413
+ cover_art.display = False
414
+ if track.artwork_url:
415
+ self._load_cover_art(track.artwork_url)
416
+
417
+ @work(thread=True)
418
+ def _load_cover_art(self, artwork_url: str) -> None:
419
+ try:
420
+ with urlopen(artwork_url, timeout=10) as response:
421
+ image_bytes = response.read()
422
+ except Exception: # noqa: BLE001 - missing artwork should not stop playback
423
+ return
424
+ self.call_from_thread(self._set_cover_art, artwork_url, image_bytes)
425
+
426
+ def _set_cover_art(self, artwork_url: str, image_bytes: bytes) -> None:
427
+ if self.current_track is None or self.current_track.artwork_url != artwork_url:
428
+ return
429
+ cover_art = self.query_one("#cover-art", AutoImage)
430
+ cover_art.image = BytesIO(image_bytes)
431
+ cover_art.display = True
432
+
433
+ def clear_now_playing(self) -> None:
434
+ """Clear the now-playing label and cover art."""
435
+
436
+ self.current_track = None
437
+ self.query_one("#now-playing", Static).update("Nothing playing")
438
+ cover_art = self.query_one("#cover-art", AutoImage)
439
+ cover_art.image = None
440
+ cover_art.display = False
441
+
392
442
  def focused_item(self) -> BrowserItem | None:
393
443
  browser = self.query_one("#browser-list", ListView)
394
444
  row = browser.highlighted_child
@@ -48,6 +48,7 @@ class QueueTrack:
48
48
  artist: str
49
49
  album: str
50
50
  stream_url: str
51
+ artwork_url: str = ""
51
52
 
52
53
  @property
53
54
  def label(self) -> str:
@@ -136,6 +136,7 @@ class PlexMusicClient:
136
136
  artist=_safe_title(track, "grandparentTitle"),
137
137
  album=_safe_title(track, "parentTitle"),
138
138
  stream_url=track.getStreamURL(),
139
+ artwork_url=_artwork_url(track),
139
140
  )
140
141
 
141
142
  def _default_music_library(self, preferred_name: str | None) -> MusicSection:
@@ -184,6 +185,17 @@ def _safe_title(item: Any, attr: str) -> str:
184
185
  return str(getattr(item, attr, "") or "")
185
186
 
186
187
 
188
+ def _artwork_url(track: Track) -> str:
189
+ for attr in ("squareArtUrl", "thumbUrl", "artUrl"):
190
+ try:
191
+ url = getattr(track, attr, None)
192
+ except Exception: # noqa: BLE001 - artwork is optional metadata
193
+ continue
194
+ if url:
195
+ return str(url)
196
+ return ""
197
+
198
+
187
199
  def _is_track(item: Any) -> bool:
188
200
  return isinstance(item, Track) or getattr(item, "TYPE", None) == "track"
189
201
 
File without changes
File without changes
File without changes
File without changes