plex-tui 0.3.35__tar.gz → 0.3.37__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.3.35 → plex_tui-0.3.37}/CHANGELOG.md +5 -0
  2. {plex_tui-0.3.35 → plex_tui-0.3.37}/PKG-INFO +2 -1
  3. {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/aur/.SRCINFO +3 -3
  4. {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/aur/PKGBUILD +2 -2
  5. {plex_tui-0.3.35 → plex_tui-0.3.37}/pyproject.toml +2 -1
  6. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/__init__.py +1 -1
  7. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/auth.py +48 -20
  8. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_auth.py +57 -1
  9. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/aur.yml +0 -0
  10. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/bump.yml +0 -0
  11. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/ci.yml +0 -0
  12. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/post-release-aur.yml +0 -0
  13. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/post-release-homebrew.yml +0 -0
  14. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/publish-aur.yml +0 -0
  15. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/publish-pypi.yml +0 -0
  16. {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/publish-testpypi.yml +0 -0
  17. {plex_tui-0.3.35 → plex_tui-0.3.37}/.gitignore +0 -0
  18. {plex_tui-0.3.35 → plex_tui-0.3.37}/AGENTS.md +0 -0
  19. {plex_tui-0.3.35 → plex_tui-0.3.37}/LICENSE +0 -0
  20. {plex_tui-0.3.35 → plex_tui-0.3.37}/Makefile +0 -0
  21. {plex_tui-0.3.35 → plex_tui-0.3.37}/PACKAGING.md +0 -0
  22. {plex_tui-0.3.35 → plex_tui-0.3.37}/README.md +0 -0
  23. {plex_tui-0.3.35 → plex_tui-0.3.37}/RELEASE.md +0 -0
  24. {plex_tui-0.3.35 → plex_tui-0.3.37}/ROADMAP.md +0 -0
  25. {plex_tui-0.3.35 → plex_tui-0.3.37}/config.example.toml +0 -0
  26. {plex_tui-0.3.35 → plex_tui-0.3.37}/docs/assets/grid-view.png +0 -0
  27. {plex_tui-0.3.35 → plex_tui-0.3.37}/docs/assets/list-view.png +0 -0
  28. {plex_tui-0.3.35 → plex_tui-0.3.37}/docs/plex-integration-research.md +0 -0
  29. {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/aur/README.md +0 -0
  30. {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/homebrew/README.md +0 -0
  31. {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/check_release.py +0 -0
  32. {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/stage_release.py +0 -0
  33. {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/update_aur_package.py +0 -0
  34. {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/update_homebrew_formula.py +0 -0
  35. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/__main__.py +0 -0
  36. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/app.py +0 -0
  37. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/artwork.py +0 -0
  38. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/config.py +0 -0
  39. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/models.py +0 -0
  40. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/player.py +0 -0
  41. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/plex_service.py +0 -0
  42. {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/smoke.py +0 -0
  43. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_app_helpers.py +0 -0
  44. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_app_navigation.py +0 -0
  45. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_artwork.py +0 -0
  46. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_cli.py +0 -0
  47. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_config.py +0 -0
  48. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_player.py +0 -0
  49. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_plex_service.py +0 -0
  50. {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_release_checks.py +0 -0
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.37 - 2026-06-15
6
+
7
+ - Fell back to direct Plex root checks during browser login when PlexAPI's
8
+ resource connection probe rejects URLs that are reachable from the machine.
9
+
5
10
  ## 0.3.35 - 2026-06-15
6
11
 
7
12
  - Documented Homebrew 6 tap trust requirements for installing from the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plex-tui
3
- Version: 0.3.35
3
+ Version: 0.3.37
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
@@ -26,6 +26,7 @@ Requires-Python: >=3.11
26
26
  Requires-Dist: pillow>=10.0
27
27
  Requires-Dist: platformdirs>=4.2
28
28
  Requires-Dist: plexapi>=4.15
29
+ Requires-Dist: requests>=2.0
29
30
  Requires-Dist: textual>=0.86
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: build>=1.2; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  pkgbase = plex-tui
2
2
  pkgdesc = Standalone Plex terminal UI with mpv playback
3
- pkgver = 0.3.31
3
+ pkgver = 0.3.35
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.31.tar.gz::https://github.com/so1omon563/plex-tui/archive/refs/tags/v0.3.31.tar.gz
20
- sha256sums = 89cf8080c48c7d57a0bf9f4e6b273e9842fef7ecfdf783bf3add6c2425a30569
19
+ source = plex-tui-0.3.35.tar.gz::https://github.com/so1omon563/plex-tui/archive/refs/tags/v0.3.35.tar.gz
20
+ sha256sums = 9ef3e6be46a7fe3176c9d69dc491a006054afe82c79618d9ee2117a088a067fc
21
21
 
22
22
  pkgname = plex-tui
@@ -1,6 +1,6 @@
1
1
  # Maintainer: so1omon
2
2
  pkgname=plex-tui
3
- pkgver=0.3.31
3
+ pkgver=0.3.35
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=("89cf8080c48c7d57a0bf9f4e6b273e9842fef7ecfdf783bf3add6c2425a30569")
25
+ sha256sums=("9ef3e6be46a7fe3176c9d69dc491a006054afe82c79618d9ee2117a088a067fc")
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.3.35"
7
+ version = "0.3.37"
8
8
  description = "A small standalone Plex terminal UI."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "Pillow>=10.0",
33
33
  "plexapi>=4.15",
34
34
  "platformdirs>=4.2",
35
+ "requests>=2.0",
35
36
  "textual>=0.86",
36
37
  ]
37
38
 
@@ -1,3 +1,3 @@
1
1
  """Plex TUI prototype."""
2
2
 
3
- __version__ = "0.3.35"
3
+ __version__ = "0.3.37"
@@ -5,6 +5,7 @@ import webbrowser
5
5
  from dataclasses import dataclass
6
6
  from urllib.parse import SplitResult, urlparse
7
7
 
8
+ import requests
8
9
  from plexapi.myplex import MyPlexAccount, MyPlexPinLogin, MyPlexResource
9
10
 
10
11
  from . import __version__
@@ -168,30 +169,57 @@ def reachable_server_choices(resources: list[MyPlexResource], timeout: int = 5)
168
169
  choices = []
169
170
  seen: set[tuple[str, str]] = set()
170
171
  for resource in resources:
171
- try:
172
- server = resource.connect(timeout=timeout)
173
- except Exception:
174
- continue
175
-
176
- uri = str(getattr(server, "_baseurl", "") or getattr(server, "baseurl", "") or "").rstrip("/")
177
- if not uri:
178
- continue
179
- key = (resource.name, uri)
180
- if key in seen:
181
- continue
182
- seen.add(key)
183
- choices.append(
184
- ServerChoice(
185
- name=resource.name,
186
- uri=uri,
187
- source=resource.sourceTitle or "owned",
188
- resource=resource,
189
- verified=True,
172
+ for uri in reachable_resource_urls(resource, timeout):
173
+ key = (resource.name, uri)
174
+ if key in seen:
175
+ continue
176
+ seen.add(key)
177
+ choices.append(
178
+ ServerChoice(
179
+ name=resource.name,
180
+ uri=uri,
181
+ source=resource.sourceTitle or "owned",
182
+ resource=resource,
183
+ verified=True,
184
+ )
190
185
  )
191
- )
192
186
  return choices
193
187
 
194
188
 
189
+ def reachable_resource_urls(resource: MyPlexResource, timeout: int) -> list[str]:
190
+ try:
191
+ server = resource.connect(timeout=timeout)
192
+ except Exception:
193
+ return reachable_advertised_urls(resource, timeout)
194
+
195
+ uri = str(getattr(server, "_baseurl", "") or getattr(server, "baseurl", "") or "").rstrip("/")
196
+ return [uri] if uri else reachable_advertised_urls(resource, timeout)
197
+
198
+
199
+ def reachable_advertised_urls(resource: MyPlexResource, timeout: int) -> list[str]:
200
+ reachable = []
201
+ for uri in resource.preferred_connections():
202
+ normalized = uri.rstrip("/")
203
+ if normalized and plex_root_responds(normalized, resource.accessToken, timeout):
204
+ reachable.append(normalized)
205
+ return reachable
206
+
207
+
208
+ def plex_root_responds(uri: str, token: str, timeout: int) -> bool:
209
+ try:
210
+ response = requests.get(
211
+ uri,
212
+ headers={"X-Plex-Token": token},
213
+ params={"X-Plex-Token": token},
214
+ timeout=timeout,
215
+ )
216
+ except requests.RequestException:
217
+ return False
218
+ if response.status_code not in {200, 201, 204}:
219
+ return False
220
+ return "MediaContainer" in response.text or response.headers.get("X-Plex-Protocol") == "1.0"
221
+
222
+
195
223
  def plex_headers(config: AppConfig) -> dict[str, str]:
196
224
  return {
197
225
  "X-Plex-Product": APP_NAME,
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from plextui import __version__
4
- from plextui.auth import LoginSession, reachable_server_choices, plex_headers
4
+ from plextui.auth import LoginSession, reachable_advertised_urls, reachable_server_choices, plex_headers
5
5
  from plextui.config import AppConfig
6
6
 
7
7
 
@@ -134,6 +134,33 @@ def test_login_wait_fails_when_no_resource_connections_are_reachable(monkeypatch
134
134
  raise AssertionError("expected unreachable login resources to fail")
135
135
 
136
136
 
137
+ def test_login_wait_falls_back_to_reachable_advertised_urls(monkeypatch):
138
+ resource = FakeResource(
139
+ "My Plex",
140
+ "server-token",
141
+ [
142
+ "http://192.168.0.13:32400",
143
+ "http://24.255.40.151:17734",
144
+ ],
145
+ )
146
+ resources = [resource]
147
+ responses = {
148
+ "http://192.168.0.13:32400": FakeResponse(200, "<MediaContainer friendlyName='Plex' />"),
149
+ "http://24.255.40.151:17734": FakeResponse(404, "not found"),
150
+ }
151
+
152
+ monkeypatch.setattr("plextui.auth.MyPlexAccount", lambda token: FakeAccount(resources))
153
+ monkeypatch.setattr("plextui.auth.requests.get", lambda uri, **kwargs: responses[uri])
154
+ session = object.__new__(LoginSession)
155
+ session.config = AppConfig("", "", "client-id")
156
+ session.pin_login = FakePinLogin("account-token")
157
+
158
+ _account_token, choices = session.wait()
159
+
160
+ assert [choice.uri for choice in choices] == ["http://192.168.0.13:32400"]
161
+ assert choices[0].verified
162
+
163
+
137
164
  def test_reachable_server_choices_deduplicates_connected_urls():
138
165
  resources = [
139
166
  FakeResource("My Plex", "server-token", [], reachable_uri="http://plex.example:32400"),
@@ -143,3 +170,32 @@ def test_reachable_server_choices_deduplicates_connected_urls():
143
170
  choices = reachable_server_choices(resources, timeout=1)
144
171
 
145
172
  assert [choice.uri for choice in choices] == ["http://plex.example:32400"]
173
+
174
+
175
+ def test_reachable_advertised_urls_accepts_plex_protocol_header(monkeypatch):
176
+ resource = FakeResource("My Plex", "server-token", ["http://plex.example:32400"])
177
+ seen = {}
178
+
179
+ def fake_get(uri, **kwargs):
180
+ seen["uri"] = uri
181
+ seen["headers"] = kwargs["headers"]
182
+ seen["params"] = kwargs["params"]
183
+ seen["timeout"] = kwargs["timeout"]
184
+ return FakeResponse(200, "", {"X-Plex-Protocol": "1.0"})
185
+
186
+ monkeypatch.setattr("plextui.auth.requests.get", fake_get)
187
+
188
+ assert reachable_advertised_urls(resource, timeout=2) == ["http://plex.example:32400"]
189
+ assert seen == {
190
+ "uri": "http://plex.example:32400",
191
+ "headers": {"X-Plex-Token": "server-token"},
192
+ "params": {"X-Plex-Token": "server-token"},
193
+ "timeout": 2,
194
+ }
195
+
196
+
197
+ class FakeResponse:
198
+ def __init__(self, status_code: int, text: str, headers: dict[str, str] | None = None) -> None:
199
+ self.status_code = status_code
200
+ self.text = text
201
+ self.headers = headers or {}
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