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.
- {plexbar-0.1.3 → plexbar-0.1.4}/PKG-INFO +3 -1
- {plexbar-0.1.3 → plexbar-0.1.4}/README.md +1 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/pyproject.toml +8 -8
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/app.py +50 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/models.py +1 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/plex_client.py +12 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/__init__.py +0 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/__main__.py +0 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/playback.py +0 -0
- {plexbar-0.1.3 → plexbar-0.1.4}/src/plexbar/settings.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: plexbar
|
|
3
|
-
Version: 0.1.
|
|
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
|
+
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 = [
|
|
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
|
|
@@ -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
|