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.
Files changed (50) hide show
  1. unit3dprep/__init__.py +0 -0
  2. unit3dprep/cli.py +256 -0
  3. unit3dprep/core.py +556 -0
  4. unit3dprep/i18n.py +151 -0
  5. unit3dprep/media.py +278 -0
  6. unit3dprep/upload.py +146 -0
  7. unit3dprep/web/__init__.py +0 -0
  8. unit3dprep/web/_env.py +40 -0
  9. unit3dprep/web/api/__init__.py +1 -0
  10. unit3dprep/web/api/auth.py +36 -0
  11. unit3dprep/web/api/fs.py +64 -0
  12. unit3dprep/web/api/library.py +503 -0
  13. unit3dprep/web/api/logs.py +42 -0
  14. unit3dprep/web/api/queue.py +54 -0
  15. unit3dprep/web/api/quickupload.py +153 -0
  16. unit3dprep/web/api/search.py +40 -0
  17. unit3dprep/web/api/settings.py +77 -0
  18. unit3dprep/web/api/tmdb.py +77 -0
  19. unit3dprep/web/api/trackers.py +30 -0
  20. unit3dprep/web/api/uploaded.py +26 -0
  21. unit3dprep/web/api/version.py +754 -0
  22. unit3dprep/web/api/webup.py +59 -0
  23. unit3dprep/web/api/wizard.py +512 -0
  24. unit3dprep/web/app.py +201 -0
  25. unit3dprep/web/auth.py +41 -0
  26. unit3dprep/web/clients.py +167 -0
  27. unit3dprep/web/config.py +932 -0
  28. unit3dprep/web/db.py +184 -0
  29. unit3dprep/web/dist/assets/JetBrainsMono-Italic-VariableFont_wght-CZO9PUqx.ttf +0 -0
  30. unit3dprep/web/dist/assets/JetBrainsMono-VariableFont_wght-BrlcHZ7m.ttf +0 -0
  31. unit3dprep/web/dist/assets/SpaceGrotesk-VariableFont_wght-DIScfSlK.ttf +0 -0
  32. unit3dprep/web/dist/assets/index-BizNr_oP.js +255 -0
  33. unit3dprep/web/dist/assets/index-DChRHChM.css +1 -0
  34. unit3dprep/web/dist/index.html +14 -0
  35. unit3dprep/web/duplicate_check.py +92 -0
  36. unit3dprep/web/lang_cache.py +118 -0
  37. unit3dprep/web/logbuf.py +164 -0
  38. unit3dprep/web/tmdb_cache.py +116 -0
  39. unit3dprep/web/trackers.py +177 -0
  40. unit3dprep/web/webup_client.py +190 -0
  41. unit3dprep/web/webup_job_fix.py +231 -0
  42. unit3dprep/web/webup_logclass.py +107 -0
  43. unit3dprep/web/webup_orchestrator.py +796 -0
  44. unit3dprep/web/webup_ws.py +141 -0
  45. unit3dprep-1.0.4.dist-info/METADATA +819 -0
  46. unit3dprep-1.0.4.dist-info/RECORD +50 -0
  47. unit3dprep-1.0.4.dist-info/WHEEL +5 -0
  48. unit3dprep-1.0.4.dist-info/entry_points.txt +3 -0
  49. unit3dprep-1.0.4.dist-info/licenses/LICENSE +674 -0
  50. 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