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.
- {plex_tui-0.3.35 → plex_tui-0.3.37}/CHANGELOG.md +5 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/PKG-INFO +2 -1
- {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/aur/.SRCINFO +3 -3
- {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/aur/PKGBUILD +2 -2
- {plex_tui-0.3.35 → plex_tui-0.3.37}/pyproject.toml +2 -1
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/__init__.py +1 -1
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/auth.py +48 -20
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_auth.py +57 -1
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/aur.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/bump.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/ci.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/post-release-aur.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/post-release-homebrew.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/publish-aur.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/publish-pypi.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.github/workflows/publish-testpypi.yml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/.gitignore +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/AGENTS.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/LICENSE +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/Makefile +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/PACKAGING.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/README.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/RELEASE.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/ROADMAP.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/config.example.toml +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/docs/assets/grid-view.png +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/docs/assets/list-view.png +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/docs/plex-integration-research.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/aur/README.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/packaging/homebrew/README.md +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/check_release.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/stage_release.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/update_aur_package.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/scripts/update_homebrew_formula.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/__main__.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/app.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/artwork.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/config.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/models.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/player.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/plex_service.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/src/plextui/smoke.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_app_helpers.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_app_navigation.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_artwork.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_cli.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_config.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_player.py +0 -0
- {plex_tui-0.3.35 → plex_tui-0.3.37}/tests/test_plex_service.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
20
|
-
sha256sums =
|
|
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.
|
|
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=("
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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
|
|
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
|