plex-tui 0.4.0__tar.gz → 0.4.2__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.
Files changed (50) hide show
  1. {plex_tui-0.4.0 → plex_tui-0.4.2}/CHANGELOG.md +11 -0
  2. {plex_tui-0.4.0 → plex_tui-0.4.2}/PKG-INFO +8 -6
  3. {plex_tui-0.4.0 → plex_tui-0.4.2}/README.md +7 -5
  4. {plex_tui-0.4.0 → plex_tui-0.4.2}/ROADMAP.md +8 -1
  5. {plex_tui-0.4.0 → plex_tui-0.4.2}/config.example.toml +3 -2
  6. {plex_tui-0.4.0 → plex_tui-0.4.2}/packaging/aur/.SRCINFO +3 -3
  7. {plex_tui-0.4.0 → plex_tui-0.4.2}/packaging/aur/PKGBUILD +2 -2
  8. {plex_tui-0.4.0 → plex_tui-0.4.2}/pyproject.toml +1 -1
  9. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/__init__.py +1 -1
  10. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/artwork.py +12 -4
  11. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/plex_service.py +7 -1
  12. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_artwork.py +28 -5
  13. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_plex_service.py +48 -1
  14. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/aur.yml +0 -0
  15. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/bump.yml +0 -0
  16. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/ci.yml +0 -0
  17. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/post-release-aur.yml +0 -0
  18. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/post-release-homebrew.yml +0 -0
  19. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/publish-aur.yml +0 -0
  20. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/publish-pypi.yml +0 -0
  21. {plex_tui-0.4.0 → plex_tui-0.4.2}/.github/workflows/publish-testpypi.yml +0 -0
  22. {plex_tui-0.4.0 → plex_tui-0.4.2}/.gitignore +0 -0
  23. {plex_tui-0.4.0 → plex_tui-0.4.2}/AGENTS.md +0 -0
  24. {plex_tui-0.4.0 → plex_tui-0.4.2}/LICENSE +0 -0
  25. {plex_tui-0.4.0 → plex_tui-0.4.2}/Makefile +0 -0
  26. {plex_tui-0.4.0 → plex_tui-0.4.2}/PACKAGING.md +0 -0
  27. {plex_tui-0.4.0 → plex_tui-0.4.2}/RELEASE.md +0 -0
  28. {plex_tui-0.4.0 → plex_tui-0.4.2}/docs/assets/grid-view.png +0 -0
  29. {plex_tui-0.4.0 → plex_tui-0.4.2}/docs/assets/list-view.png +0 -0
  30. {plex_tui-0.4.0 → plex_tui-0.4.2}/docs/plex-integration-research.md +0 -0
  31. {plex_tui-0.4.0 → plex_tui-0.4.2}/packaging/aur/README.md +0 -0
  32. {plex_tui-0.4.0 → plex_tui-0.4.2}/packaging/homebrew/README.md +0 -0
  33. {plex_tui-0.4.0 → plex_tui-0.4.2}/scripts/check_release.py +0 -0
  34. {plex_tui-0.4.0 → plex_tui-0.4.2}/scripts/stage_release.py +0 -0
  35. {plex_tui-0.4.0 → plex_tui-0.4.2}/scripts/update_aur_package.py +0 -0
  36. {plex_tui-0.4.0 → plex_tui-0.4.2}/scripts/update_homebrew_formula.py +0 -0
  37. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/__main__.py +0 -0
  38. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/app.py +0 -0
  39. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/auth.py +0 -0
  40. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/config.py +0 -0
  41. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/models.py +0 -0
  42. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/player.py +0 -0
  43. {plex_tui-0.4.0 → plex_tui-0.4.2}/src/plextui/smoke.py +0 -0
  44. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_app_helpers.py +0 -0
  45. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_app_navigation.py +0 -0
  46. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_auth.py +0 -0
  47. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_cli.py +0 -0
  48. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_config.py +0 -0
  49. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_player.py +0 -0
  50. {plex_tui-0.4.0 → plex_tui-0.4.2}/tests/test_release_checks.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.4.2 - 2026-06-18
6
+
7
+ - Prefer episode stills and season posters for TV artwork while keeping show art
8
+ as the fallback.
9
+
10
+ ## 0.4.1 - 2026-06-18
11
+
12
+ - Let explicit `artwork_renderer = "kitty"` try Kitty graphics even when
13
+ Kitty-specific environment variables are absent, and let `auto` enable the
14
+ Kitty graphics path in Ghostty.
15
+
5
16
  ## 0.4.0 - 2026-06-17
6
17
 
7
18
  - Added native Kitty poster artwork via Unicode placeholders for terminals that
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plex-tui
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A small standalone Plex terminal UI.
5
5
  Project-URL: Homepage, https://github.com/so1omon563/plex-tui
6
6
  Project-URL: Issues, https://github.com/so1omon563/plex-tui/issues
@@ -247,11 +247,13 @@ presets.
247
247
  ## Artwork
248
248
 
249
249
  Poster artwork renders as portable colored block art by default, so it works in
250
- ordinary terminals without native image support. In Kitty, set
251
- `artwork_renderer` to `auto` or `kitty` to render native terminal images through
252
- Kitty Unicode placeholders. This keeps Textual in charge of layout and redraws
253
- while Kitty paints the poster inside those cells. Outside Kitty, `auto` falls
254
- back to block art; `plex-tui --diagnostics` reports the active renderer status.
250
+ ordinary terminals without native image support. In Kitty and Ghostty, set
251
+ `artwork_renderer` to `auto` to render native terminal images through Kitty
252
+ Unicode placeholders. Set `artwork_renderer` to `kitty` to explicitly try the
253
+ Kitty graphics protocol in other compatible terminals. This keeps Textual in
254
+ charge of layout and redraws while the terminal paints the poster inside those
255
+ cells. Outside detected Kitty-compatible terminals, `auto` falls back to block
256
+ art; `plex-tui --diagnostics` reports the active renderer status.
255
257
 
256
258
  Grid view prefetches artwork for the visible page immediately and, by default,
257
259
  prepares three pages ahead in the background. `grid_prefetch_pages` can be set
@@ -210,11 +210,13 @@ presets.
210
210
  ## Artwork
211
211
 
212
212
  Poster artwork renders as portable colored block art by default, so it works in
213
- ordinary terminals without native image support. In Kitty, set
214
- `artwork_renderer` to `auto` or `kitty` to render native terminal images through
215
- Kitty Unicode placeholders. This keeps Textual in charge of layout and redraws
216
- while Kitty paints the poster inside those cells. Outside Kitty, `auto` falls
217
- back to block art; `plex-tui --diagnostics` reports the active renderer status.
213
+ ordinary terminals without native image support. In Kitty and Ghostty, set
214
+ `artwork_renderer` to `auto` to render native terminal images through Kitty
215
+ Unicode placeholders. Set `artwork_renderer` to `kitty` to explicitly try the
216
+ Kitty graphics protocol in other compatible terminals. This keeps Textual in
217
+ charge of layout and redraws while the terminal paints the poster inside those
218
+ cells. Outside detected Kitty-compatible terminals, `auto` falls back to block
219
+ art; `plex-tui --diagnostics` reports the active renderer status.
218
220
 
219
221
  Grid view prefetches artwork for the visible page immediately and, by default,
220
222
  prepares three pages ahead in the background. `grid_prefetch_pages` can be set
@@ -65,5 +65,12 @@
65
65
  artwork, pagination, or detail-loading behavior.
66
66
  - Revisit whether verbose `PLEX_TUI_ARTWORK_LOG=1` should expose more structured artwork counters.
67
67
  - Continue validating Kitty Unicode-placeholder artwork in real terminals.
68
- Block art remains the mandatory fallback outside Kitty.
68
+ Block art remains the mandatory fallback outside known native-image paths.
69
+ - Continue expanding Kitty graphics support beyond Kitty itself after the
70
+ initial Ghostty path. `artwork_renderer = "kitty"` is the explicit "try Kitty
71
+ protocol" override, while `auto` should stay conservative until terminal
72
+ support is verified.
73
+ - Research explicit iTerm inline-image protocol support as a separate renderer
74
+ path rather than assuming iTerm speaks Kitty graphics. Include WezTerm in that
75
+ research because its documented image path follows the iTerm image protocol.
69
76
  - Add focused regression tests for any real-world Plex media edge cases discovered during use.
@@ -40,8 +40,9 @@ client_identifier = "plex-tui-your-stable-client-id"
40
40
  # Optional artwork settings.
41
41
  # artwork_mode can be "on" or "off".
42
42
  # artwork_mode = "on"
43
- # artwork_renderer can be "block", "auto", or "kitty". Auto uses native Kitty
44
- # images when Kitty is detected, otherwise it falls back to block art.
43
+ # artwork_renderer can be "block", "auto", or "kitty". Auto uses Kitty graphics
44
+ # in detected compatible terminals such as Kitty and Ghostty, otherwise it falls
45
+ # back to block art. "kitty" explicitly tries the Kitty graphics protocol.
45
46
  # artwork_renderer = "block"
46
47
  # detail_artwork_mode can be "list_only", "on", or "off".
47
48
  # detail_artwork_mode = "list_only"
@@ -1,6 +1,6 @@
1
1
  pkgbase = plex-tui
2
2
  pkgdesc = Standalone Plex terminal UI with mpv playback
3
- pkgver = 0.3.43
3
+ pkgver = 0.4.1
4
4
  pkgrel = 1
5
5
  url = https://github.com/so1omon563/plex-tui
6
6
  arch = any
@@ -16,7 +16,7 @@ pkgbase = plex-tui
16
16
  depends = python-plexapi
17
17
  depends = python-rich
18
18
  depends = python-textual
19
- source = plex-tui-0.3.43.tar.gz::https://github.com/so1omon563/plex-tui/archive/refs/tags/v0.3.43.tar.gz
20
- sha256sums = 7e034f82945a4645fd01364c79b1d9bfd2d7820257aef4012eaf9ce5d3e9aa58
19
+ source = plex-tui-0.4.1.tar.gz::https://github.com/so1omon563/plex-tui/archive/refs/tags/v0.4.1.tar.gz
20
+ sha256sums = 2271c6413ec204ba1250b812e6b9e98af37afaeb44006d5c17fb0f82cf419282
21
21
 
22
22
  pkgname = plex-tui
@@ -1,6 +1,6 @@
1
1
  # Maintainer: so1omon
2
2
  pkgname=plex-tui
3
- pkgver=0.3.43
3
+ pkgver=0.4.1
4
4
  pkgrel=1
5
5
  pkgdesc="Standalone Plex terminal UI with mpv playback"
6
6
  arch=("any")
@@ -22,7 +22,7 @@ makedepends=(
22
22
  "python-wheel"
23
23
  )
24
24
  source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
25
- sha256sums=("7e034f82945a4645fd01364c79b1d9bfd2d7820257aef4012eaf9ce5d3e9aa58")
25
+ sha256sums=("2271c6413ec204ba1250b812e6b9e98af37afaeb44006d5c17fb0f82cf419282")
26
26
 
27
27
  build() {
28
28
  cd "$pkgname-$pkgver"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plex-tui"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "A small standalone Plex terminal UI."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """Plex TUI prototype."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.2"
@@ -210,7 +210,7 @@ def render_protocol_artwork(data: bytes, renderer: str, width: int = 28, max_hei
210
210
 
211
211
 
212
212
  def resolve_protocol_renderer(renderer: str) -> str:
213
- if renderer == "kitty" and kitty_graphics_supported():
213
+ if renderer == "kitty":
214
214
  return "kitty"
215
215
  if renderer == "auto" and kitty_graphics_supported():
216
216
  return "kitty"
@@ -222,20 +222,28 @@ def protocol_renderer_status(renderer: str) -> str:
222
222
  if resolved == "kitty":
223
223
  return "Kitty native images via Unicode placeholders"
224
224
  if renderer == "auto":
225
- return "Block art; Kitty terminal not detected"
225
+ return "Block art; Kitty-compatible terminal not detected"
226
226
  if renderer == "kitty":
227
- return "Block fallback; Kitty terminal not detected"
227
+ return "Kitty native images via Unicode placeholders"
228
228
  return "Block art"
229
229
 
230
230
 
231
231
  def kitty_graphics_supported() -> bool:
232
- return bool(os.environ.get("KITTY_WINDOW_ID") or os.environ.get("KITTY_PID"))
232
+ return bool(os.environ.get("KITTY_WINDOW_ID") or os.environ.get("KITTY_PID") or ghostty_terminal_detected())
233
+
234
+
235
+ def ghostty_terminal_detected() -> bool:
236
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
237
+ term = os.environ.get("TERM", "").lower()
238
+ return term_program == "ghostty" or term.startswith("xterm-ghostty")
233
239
 
234
240
 
235
241
  def kitty_environment_status() -> str:
236
242
  fields = [
237
243
  f"KITTY_WINDOW_ID={int(bool(os.environ.get('KITTY_WINDOW_ID')))}",
238
244
  f"KITTY_PID={int(bool(os.environ.get('KITTY_PID')))}",
245
+ f"TERM_PROGRAM={os.environ.get('TERM_PROGRAM', '')!r}",
246
+ f"TERM_PROGRAM_VERSION={os.environ.get('TERM_PROGRAM_VERSION', '')!r}",
239
247
  f"TERM={os.environ.get('TERM', '')!r}",
240
248
  f"COLORTERM={os.environ.get('COLORTERM', '')!r}",
241
249
  ]
@@ -226,7 +226,13 @@ def media_details(item: MediaItem) -> MediaDetails:
226
226
 
227
227
 
228
228
  def artwork_path(raw: Any) -> str:
229
- for attr in ("grandparentThumb", "parentThumb", "thumb", "art"):
229
+ kind = str(getattr(raw, "TYPE", raw.__class__.__name__)).lower()
230
+ attrs = (
231
+ ("thumb", "parentThumb", "grandparentThumb", "art")
232
+ if kind in {"show", "season", "episode"}
233
+ else ("grandparentThumb", "parentThumb", "thumb", "art")
234
+ )
235
+ for attr in attrs:
230
236
  value = getattr(raw, attr, None)
231
237
  if value:
232
238
  return str(value)
@@ -80,16 +80,20 @@ def test_render_artwork_returns_halfcell_text():
80
80
  assert rendered.spans
81
81
 
82
82
 
83
- def test_render_protocol_artwork_falls_back_without_explicit_native_opt_in(monkeypatch):
83
+ def test_render_protocol_artwork_tries_kitty_when_explicitly_enabled(monkeypatch):
84
84
  monkeypatch.delenv("KITTY_WINDOW_ID", raising=False)
85
85
  monkeypatch.delenv("KITTY_PID", raising=False)
86
+ monkeypatch.delenv("TERM_PROGRAM", raising=False)
87
+ transmitted = []
88
+ monkeypatch.setattr(artwork, "emit_kitty_graphics_commands", lambda commands: transmitted.extend(commands))
86
89
  image = Image.new("RGB", (2, 4), "#00ff00")
87
90
  buffer = BytesIO()
88
91
  image.save(buffer, format="PNG")
89
92
 
90
93
  rendered = render_protocol_artwork(buffer.getvalue(), "kitty", width=2, max_height=2)
91
94
 
92
- assert rendered is None
95
+ assert isinstance(rendered, KittyImage)
96
+ assert transmitted == list(rendered.commands)
93
97
 
94
98
 
95
99
  def test_render_protocol_artwork_uses_unicode_placeholders_when_kitty_is_detected(monkeypatch):
@@ -136,6 +140,7 @@ def test_kitty_graphics_commands_chunks_large_payload(monkeypatch):
136
140
  def test_auto_protocol_renderer_requires_kitty_terminal(monkeypatch):
137
141
  monkeypatch.delenv("KITTY_WINDOW_ID", raising=False)
138
142
  monkeypatch.delenv("KITTY_PID", raising=False)
143
+ monkeypatch.delenv("TERM_PROGRAM", raising=False)
139
144
  monkeypatch.setenv("TERM", "xterm-256color")
140
145
  image = Image.new("RGB", (2, 4), "#00ff00")
141
146
  buffer = BytesIO()
@@ -144,14 +149,32 @@ def test_auto_protocol_renderer_requires_kitty_terminal(monkeypatch):
144
149
  rendered = render_protocol_artwork(buffer.getvalue(), "auto", width=2, max_height=2)
145
150
 
146
151
  assert rendered is None
147
- assert protocol_renderer_status("auto") == "Block art; Kitty terminal not detected"
152
+ assert protocol_renderer_status("auto") == "Block art; Kitty-compatible terminal not detected"
153
+
154
+
155
+ def test_auto_protocol_renderer_uses_kitty_placeholders_in_ghostty(monkeypatch):
156
+ monkeypatch.delenv("KITTY_WINDOW_ID", raising=False)
157
+ monkeypatch.delenv("KITTY_PID", raising=False)
158
+ monkeypatch.setenv("TERM_PROGRAM", "ghostty")
159
+ transmitted = []
160
+ monkeypatch.setattr(artwork, "emit_kitty_graphics_commands", lambda commands: transmitted.extend(commands))
161
+ image = Image.new("RGB", (2, 4), "#00ff00")
162
+ buffer = BytesIO()
163
+ image.save(buffer, format="PNG")
164
+
165
+ rendered = render_protocol_artwork(buffer.getvalue(), "auto", width=2, max_height=2)
148
166
 
167
+ assert isinstance(rendered, KittyImage)
168
+ assert transmitted == list(rendered.commands)
169
+ assert protocol_renderer_status("auto") == "Kitty native images via Unicode placeholders"
149
170
 
150
- def test_protocol_renderer_status_explains_textual_fallback(monkeypatch):
171
+
172
+ def test_protocol_renderer_status_explains_explicit_kitty_force(monkeypatch):
151
173
  monkeypatch.delenv("KITTY_WINDOW_ID", raising=False)
152
174
  monkeypatch.delenv("KITTY_PID", raising=False)
175
+ monkeypatch.delenv("TERM_PROGRAM", raising=False)
153
176
 
154
- assert protocol_renderer_status("kitty") == "Block fallback; Kitty terminal not detected"
177
+ assert protocol_renderer_status("kitty") == "Kitty native images via Unicode placeholders"
155
178
 
156
179
 
157
180
  def test_kitty_placeholder_lines_encode_row_and_column_cells():
@@ -1,7 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from plextui.models import LibraryItem, MediaItem
4
- from plextui.plex_service import PlexService, kind_label, media_details, progress_label, row_progress_marker, watched_state
4
+ from plextui.plex_service import (
5
+ PlexService,
6
+ artwork_path,
7
+ kind_label,
8
+ media_details,
9
+ progress_label,
10
+ row_progress_marker,
11
+ to_media_item,
12
+ watched_state,
13
+ )
5
14
 
6
15
 
7
16
  class RawItem:
@@ -62,6 +71,18 @@ class DetailedRawItem(RawItem):
62
71
  return [Part()]
63
72
 
64
73
 
74
+ class TvEpisodeRawItem(DetailedRawItem):
75
+ TYPE = "episode"
76
+ title = "Episode"
77
+ thumb = "/library/metadata/episode/thumb"
78
+
79
+
80
+ class TvSeasonRawItem(DetailedRawItem):
81
+ TYPE = "season"
82
+ title = "Season 1"
83
+ thumb = "/library/metadata/season-own/thumb"
84
+
85
+
65
86
  class RawPage(list):
66
87
  totalSize = 250
67
88
 
@@ -219,6 +240,32 @@ def test_media_details_include_audio_and_subtitle_locations():
219
240
  assert details.artwork_path == "/library/metadata/show/thumb"
220
241
 
221
242
 
243
+ def test_tv_episode_artwork_prefers_episode_still():
244
+ assert artwork_path(TvEpisodeRawItem()) == "/library/metadata/episode/thumb"
245
+ assert to_media_item(TvEpisodeRawItem()).artwork_path == "/library/metadata/episode/thumb"
246
+
247
+
248
+ def test_tv_season_artwork_prefers_season_poster():
249
+ assert artwork_path(TvSeasonRawItem()) == "/library/metadata/season-own/thumb"
250
+ assert to_media_item(TvSeasonRawItem()).artwork_path == "/library/metadata/season-own/thumb"
251
+
252
+
253
+ def test_tv_artwork_falls_back_to_parent_then_show_art():
254
+ class EpisodeWithoutStill(TvEpisodeRawItem):
255
+ thumb = ""
256
+
257
+ class SeasonWithoutPoster(TvSeasonRawItem):
258
+ thumb = ""
259
+
260
+ class EpisodeWithoutSeasonArt(TvEpisodeRawItem):
261
+ thumb = ""
262
+ parentThumb = ""
263
+
264
+ assert artwork_path(EpisodeWithoutStill()) == "/library/metadata/season/thumb"
265
+ assert artwork_path(SeasonWithoutPoster()) == "/library/metadata/season/thumb"
266
+ assert artwork_path(EpisodeWithoutSeasonArt()) == "/library/metadata/show/thumb"
267
+
268
+
222
269
  def test_kind_label_humanizes_known_and_unknown_media_types():
223
270
  assert kind_label("movie") == "Movie"
224
271
  assert kind_label("show") == "TV Show"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes