unit3dprep 1.0.4__py3-none-any.whl
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.
- unit3dprep/__init__.py +0 -0
- unit3dprep/cli.py +256 -0
- unit3dprep/core.py +556 -0
- unit3dprep/i18n.py +151 -0
- unit3dprep/media.py +278 -0
- unit3dprep/upload.py +146 -0
- unit3dprep/web/__init__.py +0 -0
- unit3dprep/web/_env.py +40 -0
- unit3dprep/web/api/__init__.py +1 -0
- unit3dprep/web/api/auth.py +36 -0
- unit3dprep/web/api/fs.py +64 -0
- unit3dprep/web/api/library.py +503 -0
- unit3dprep/web/api/logs.py +42 -0
- unit3dprep/web/api/queue.py +54 -0
- unit3dprep/web/api/quickupload.py +153 -0
- unit3dprep/web/api/search.py +40 -0
- unit3dprep/web/api/settings.py +77 -0
- unit3dprep/web/api/tmdb.py +77 -0
- unit3dprep/web/api/trackers.py +30 -0
- unit3dprep/web/api/uploaded.py +26 -0
- unit3dprep/web/api/version.py +754 -0
- unit3dprep/web/api/webup.py +59 -0
- unit3dprep/web/api/wizard.py +512 -0
- unit3dprep/web/app.py +201 -0
- unit3dprep/web/auth.py +41 -0
- unit3dprep/web/clients.py +167 -0
- unit3dprep/web/config.py +932 -0
- unit3dprep/web/db.py +184 -0
- unit3dprep/web/dist/assets/JetBrainsMono-Italic-VariableFont_wght-CZO9PUqx.ttf +0 -0
- unit3dprep/web/dist/assets/JetBrainsMono-VariableFont_wght-BrlcHZ7m.ttf +0 -0
- unit3dprep/web/dist/assets/SpaceGrotesk-VariableFont_wght-DIScfSlK.ttf +0 -0
- unit3dprep/web/dist/assets/index-BizNr_oP.js +255 -0
- unit3dprep/web/dist/assets/index-DChRHChM.css +1 -0
- unit3dprep/web/dist/index.html +14 -0
- unit3dprep/web/duplicate_check.py +92 -0
- unit3dprep/web/lang_cache.py +118 -0
- unit3dprep/web/logbuf.py +164 -0
- unit3dprep/web/tmdb_cache.py +116 -0
- unit3dprep/web/trackers.py +177 -0
- unit3dprep/web/webup_client.py +190 -0
- unit3dprep/web/webup_job_fix.py +231 -0
- unit3dprep/web/webup_logclass.py +107 -0
- unit3dprep/web/webup_orchestrator.py +796 -0
- unit3dprep/web/webup_ws.py +141 -0
- unit3dprep-1.0.4.dist-info/METADATA +819 -0
- unit3dprep-1.0.4.dist-info/RECORD +50 -0
- unit3dprep-1.0.4.dist-info/WHEEL +5 -0
- unit3dprep-1.0.4.dist-info/entry_points.txt +3 -0
- unit3dprep-1.0.4.dist-info/licenses/LICENSE +674 -0
- unit3dprep-1.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Persistent WebSocket listener for Unit3DWebUp /ws.
|
|
2
|
+
|
|
3
|
+
Spawned in app.py lifespan. Maintains a fan-out registry so the orchestrator
|
|
4
|
+
can subscribe per `job_id` (or wildcard) and receive every relevant message
|
|
5
|
+
via an asyncio.Queue. Auto-reconnects with exponential backoff.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import websockets
|
|
15
|
+
from websockets.exceptions import ConnectionClosed
|
|
16
|
+
|
|
17
|
+
from .webup_client import base_url
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
WILDCARD = "*"
|
|
21
|
+
_log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ws_url() -> str:
|
|
25
|
+
base = base_url()
|
|
26
|
+
if base.startswith("https://"):
|
|
27
|
+
return "wss://" + base[len("https://"):] + "/ws"
|
|
28
|
+
if base.startswith("http://"):
|
|
29
|
+
return "ws://" + base[len("http://"):] + "/ws"
|
|
30
|
+
return base + "/ws"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WebupWSManager:
|
|
34
|
+
"""Singleton-ish; created in lifespan and stored on app.state.webup_ws."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._subs: dict[str, list[asyncio.Queue]] = {}
|
|
38
|
+
self._lock = asyncio.Lock()
|
|
39
|
+
self._task: asyncio.Task | None = None
|
|
40
|
+
self._stop = asyncio.Event()
|
|
41
|
+
self._connected = asyncio.Event()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def connected(self) -> bool:
|
|
45
|
+
return self._connected.is_set()
|
|
46
|
+
|
|
47
|
+
async def subscribe(self, key: str = WILDCARD) -> asyncio.Queue:
|
|
48
|
+
q: asyncio.Queue = asyncio.Queue()
|
|
49
|
+
async with self._lock:
|
|
50
|
+
self._subs.setdefault(key, []).append(q)
|
|
51
|
+
return q
|
|
52
|
+
|
|
53
|
+
async def unsubscribe(self, key: str, q: asyncio.Queue) -> None:
|
|
54
|
+
async with self._lock:
|
|
55
|
+
lst = self._subs.get(key)
|
|
56
|
+
if not lst:
|
|
57
|
+
return
|
|
58
|
+
try:
|
|
59
|
+
lst.remove(q)
|
|
60
|
+
except ValueError:
|
|
61
|
+
pass
|
|
62
|
+
if not lst:
|
|
63
|
+
self._subs.pop(key, None)
|
|
64
|
+
|
|
65
|
+
async def rekey(self, old: str, new: str, q: asyncio.Queue) -> None:
|
|
66
|
+
"""Move a queue from one key (typically wildcard) to a specific job_id."""
|
|
67
|
+
async with self._lock:
|
|
68
|
+
lst = self._subs.get(old)
|
|
69
|
+
if lst and q in lst:
|
|
70
|
+
lst.remove(q)
|
|
71
|
+
if not lst:
|
|
72
|
+
self._subs.pop(old, None)
|
|
73
|
+
self._subs.setdefault(new, []).append(q)
|
|
74
|
+
|
|
75
|
+
async def _dispatch(self, msg: dict[str, Any]) -> None:
|
|
76
|
+
keys: list[str] = [WILDCARD]
|
|
77
|
+
jid = msg.get("job_id")
|
|
78
|
+
if jid:
|
|
79
|
+
keys.append(str(jid))
|
|
80
|
+
async with self._lock:
|
|
81
|
+
queues: list[asyncio.Queue] = []
|
|
82
|
+
for k in keys:
|
|
83
|
+
queues.extend(self._subs.get(k, []))
|
|
84
|
+
mtype = (msg.get("type") or "").lower()
|
|
85
|
+
if mtype == "posterlogmessage":
|
|
86
|
+
_log.info(
|
|
87
|
+
"webup ws dispatch posterLogMessage job_id=%s msg=%r queues=%d",
|
|
88
|
+
jid, str(msg.get("message") or "")[:80], len(queues),
|
|
89
|
+
)
|
|
90
|
+
for q in queues:
|
|
91
|
+
try:
|
|
92
|
+
q.put_nowait(msg)
|
|
93
|
+
except asyncio.QueueFull:
|
|
94
|
+
_log.warning("webup ws queue full, dropping msg for key=%s", jid or WILDCARD)
|
|
95
|
+
|
|
96
|
+
async def _run(self) -> None:
|
|
97
|
+
backoff = 1.0
|
|
98
|
+
while not self._stop.is_set():
|
|
99
|
+
url = _ws_url()
|
|
100
|
+
try:
|
|
101
|
+
async with websockets.connect(url, ping_interval=20, ping_timeout=20, close_timeout=5) as ws:
|
|
102
|
+
self._connected.set()
|
|
103
|
+
backoff = 1.0
|
|
104
|
+
_log.info("webup ws connected: %s", url)
|
|
105
|
+
async for raw in ws:
|
|
106
|
+
try:
|
|
107
|
+
msg = json.loads(raw)
|
|
108
|
+
except (TypeError, ValueError):
|
|
109
|
+
continue
|
|
110
|
+
if not isinstance(msg, dict):
|
|
111
|
+
continue
|
|
112
|
+
await self._dispatch(msg)
|
|
113
|
+
except (ConnectionClosed, OSError, asyncio.TimeoutError) as e:
|
|
114
|
+
_log.debug("webup ws disconnected: %s", e)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
_log.warning("webup ws error: %s", e)
|
|
117
|
+
finally:
|
|
118
|
+
self._connected.clear()
|
|
119
|
+
if self._stop.is_set():
|
|
120
|
+
break
|
|
121
|
+
try:
|
|
122
|
+
await asyncio.wait_for(self._stop.wait(), timeout=backoff)
|
|
123
|
+
except asyncio.TimeoutError:
|
|
124
|
+
pass
|
|
125
|
+
backoff = min(backoff * 2, 30.0)
|
|
126
|
+
|
|
127
|
+
def start(self) -> None:
|
|
128
|
+
if self._task and not self._task.done():
|
|
129
|
+
return
|
|
130
|
+
self._stop.clear()
|
|
131
|
+
self._task = asyncio.create_task(self._run(), name="webup-ws-listener")
|
|
132
|
+
|
|
133
|
+
async def stop(self) -> None:
|
|
134
|
+
self._stop.set()
|
|
135
|
+
if self._task:
|
|
136
|
+
self._task.cancel()
|
|
137
|
+
try:
|
|
138
|
+
await self._task
|
|
139
|
+
except (asyncio.CancelledError, Exception):
|
|
140
|
+
pass
|
|
141
|
+
self._task = None
|