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
unit3dprep/web/db.py ADDED
@@ -0,0 +1,184 @@
1
+ """Upload history — JSON file store. No sqlite3 dependency."""
2
+ import asyncio
3
+ import json
4
+ import os
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+
9
+ _lock = threading.Lock()
10
+
11
+
12
+ def _db_path() -> Path:
13
+ """Re-resolve on every call so UI edits take effect without restart."""
14
+ default = str(Path.home() / ".unit3dprep_db.json")
15
+ try:
16
+ from . import config
17
+ return Path(config.runtime_setting("U3DP_DB_PATH", default=default))
18
+ except Exception:
19
+ from ._env import env as _env
20
+ return Path(_env("U3DP_DB_PATH", "ITA_DB_PATH", default) or default)
21
+
22
+
23
+ # Back-compat alias — resolved at import time (tests, external callers).
24
+ DB_PATH = _db_path()
25
+
26
+
27
+ def _load() -> list[dict]:
28
+ path = _db_path()
29
+ if not path.exists():
30
+ return []
31
+ try:
32
+ with open(path, "r", encoding="utf-8") as f:
33
+ return json.load(f)
34
+ except (json.JSONDecodeError, OSError):
35
+ return []
36
+
37
+
38
+ def _save(records: list[dict]):
39
+ path = _db_path()
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ with open(path, "w", encoding="utf-8") as f:
42
+ json.dump(records, f, indent=2, ensure_ascii=False)
43
+
44
+
45
+ def _now_iso() -> str:
46
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
47
+
48
+
49
+ def _init_db_sync():
50
+ with _lock:
51
+ if not _db_path().exists():
52
+ _save([])
53
+
54
+
55
+ def _record_upload_sync(
56
+ category, kind, source_path, seeding_path,
57
+ tmdb_id, title, year, final_name, exit_code, hardlink_only=False,
58
+ duplicate_skipped=False, duplicate_info=None,
59
+ ):
60
+ with _lock:
61
+ records = _load()
62
+ # upsert by seeding_path
63
+ for r in records:
64
+ if r["seeding_path"] == seeding_path:
65
+ r["unit3dup_exit_code"] = exit_code
66
+ r["uploaded_at"] = _now_iso()
67
+ r["hardlink_only"] = hardlink_only
68
+ r["duplicate_skipped"] = duplicate_skipped
69
+ if duplicate_info is not None:
70
+ r["duplicate_info"] = duplicate_info
71
+ if source_path:
72
+ r["source_path"] = source_path
73
+ if tmdb_id:
74
+ r["tmdb_id"] = tmdb_id
75
+ if title:
76
+ r["title"] = title
77
+ if year:
78
+ r["year"] = year
79
+ if final_name:
80
+ r["final_name"] = final_name
81
+ _save(records)
82
+ return
83
+ next_id = max((r.get("id", 0) for r in records), default=0) + 1
84
+ records.append({
85
+ "id": next_id,
86
+ "category": category,
87
+ "kind": kind,
88
+ "source_path": source_path,
89
+ "seeding_path": seeding_path,
90
+ "tmdb_id": tmdb_id or "",
91
+ "title": title or "",
92
+ "year": year or "",
93
+ "final_name": final_name or "",
94
+ "uploaded_at": _now_iso(),
95
+ "unit3dup_exit_code": exit_code,
96
+ "hardlink_only": hardlink_only,
97
+ "duplicate_skipped": duplicate_skipped,
98
+ "duplicate_info": duplicate_info,
99
+ })
100
+ _save(records)
101
+
102
+
103
+ def _update_exit_code_sync(seeding_path: str, exit_code: int):
104
+ with _lock:
105
+ records = _load()
106
+ for r in records:
107
+ if r["seeding_path"] == seeding_path:
108
+ r["unit3dup_exit_code"] = exit_code
109
+ _save(records)
110
+ return
111
+
112
+
113
+ def _list_uploads_sync() -> list[dict]:
114
+ with _lock:
115
+ records = _load()
116
+ return sorted(records, key=lambda r: r.get("uploaded_at", ""), reverse=True)
117
+
118
+
119
+ def _get_by_seeding_sync(seeding_path: str) -> dict | None:
120
+ with _lock:
121
+ records = _load()
122
+ for r in records:
123
+ if r["seeding_path"] == seeding_path:
124
+ return r
125
+ return None
126
+
127
+
128
+ def _delete_record_sync(seeding_path: str):
129
+ with _lock:
130
+ records = _load()
131
+ records = [r for r in records if r["seeding_path"] != seeding_path]
132
+ _save(records)
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Async wrappers
137
+ # ---------------------------------------------------------------------------
138
+
139
+ async def _run(fn, *args):
140
+ loop = asyncio.get_event_loop()
141
+ return await loop.run_in_executor(None, fn, *args)
142
+
143
+
144
+ async def init_db():
145
+ await _run(_init_db_sync)
146
+
147
+
148
+ async def record_upload(
149
+ *,
150
+ category: str,
151
+ kind: str,
152
+ source_path: str,
153
+ seeding_path: str,
154
+ tmdb_id: str = "",
155
+ title: str = "",
156
+ year: str = "",
157
+ final_name: str = "",
158
+ exit_code: int | None = None,
159
+ hardlink_only: bool = False,
160
+ duplicate_skipped: bool = False,
161
+ duplicate_info: dict | None = None,
162
+ ):
163
+ await _run(
164
+ _record_upload_sync,
165
+ category, kind, source_path, seeding_path,
166
+ tmdb_id, title, year, final_name, exit_code, hardlink_only,
167
+ duplicate_skipped, duplicate_info,
168
+ )
169
+
170
+
171
+ async def update_exit_code(seeding_path: str, exit_code: int):
172
+ await _run(_update_exit_code_sync, seeding_path, exit_code)
173
+
174
+
175
+ async def list_uploads() -> list[dict]:
176
+ return await _run(_list_uploads_sync)
177
+
178
+
179
+ async def get_upload_by_seeding_path(seeding_path: str) -> dict | None:
180
+ return await _run(_get_by_seeding_sync, seeding_path)
181
+
182
+
183
+ async def delete_record(seeding_path: str):
184
+ await _run(_delete_record_sync, seeding_path)