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/__init__.py ADDED
File without changes
unit3dprep/cli.py ADDED
@@ -0,0 +1,256 @@
1
+ """CLI entry point. Interactive prompts wrapping core logic."""
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ try:
7
+ import readline
8
+ _HAS_READLINE = True
9
+ except ImportError:
10
+ _HAS_READLINE = False
11
+
12
+ from .core import (
13
+ build_name,
14
+ extract_specs,
15
+ format_se,
16
+ has_italian_audio,
17
+ iter_video_files,
18
+ map_source,
19
+ tmdb_fetch,
20
+ tmdb_year,
21
+ )
22
+ from .upload import do_hardlink_movie, do_hardlink_series
23
+
24
+ TMDB_API_KEY = os.environ.get("TMDB_API_KEY")
25
+
26
+
27
+ def prompt_confirm(msg: str) -> bool:
28
+ try:
29
+ answer = input(msg).strip().lower()
30
+ except (EOFError, KeyboardInterrupt):
31
+ print()
32
+ return False
33
+ return answer in {"y", "yes", "s", "si", "sì"}
34
+
35
+
36
+ def prompt_edit(msg: str, default: str) -> str:
37
+ if _HAS_READLINE:
38
+ readline.set_startup_hook(lambda: readline.insert_text(default))
39
+ try:
40
+ return input(msg).strip()
41
+ finally:
42
+ readline.set_startup_hook()
43
+ print(f"Attuale: {default}")
44
+ new = input(f"{msg} (invio = mantieni): ").strip()
45
+ return new or default
46
+
47
+
48
+ def ask_tmdb_id(kind_label: str, default_title: str = "") -> tuple[str, dict]:
49
+ kind = "tv" if kind_label == "tv" else "movie"
50
+ hint = f" (guessit: '{default_title}')" if default_title else ""
51
+ while True:
52
+ try:
53
+ raw = input(f"Inserisci TMDB ID per {kind}{hint}: ").strip()
54
+ except (EOFError, KeyboardInterrupt):
55
+ print()
56
+ sys.exit(1)
57
+ if not raw:
58
+ continue
59
+ try:
60
+ data = tmdb_fetch(kind, raw, TMDB_API_KEY or "")
61
+ return kind, data
62
+ except Exception as e:
63
+ print(f"Errore TMDB: {e}. Riprova.")
64
+
65
+
66
+ def run_webup_sync(seeding_path: str, kind: str, tmdb_id: str = "") -> int:
67
+ """Drive Unit3DWebUp pipeline synchronously, printing logs to stdout.
68
+
69
+ Returns the upload exit code (0 = success). Used as a sys.exit() value.
70
+ """
71
+ import asyncio
72
+
73
+ from .web.webup_client import WebupClient
74
+ from .web.webup_orchestrator import stream_webup
75
+ from .web.webup_ws import WebupWSManager
76
+
77
+ async def _drain() -> int:
78
+ client = WebupClient()
79
+ ws = WebupWSManager()
80
+ ws.start()
81
+ scan_lock = asyncio.Lock()
82
+ code = -1
83
+ try:
84
+ async for ev in stream_webup(
85
+ client=client,
86
+ ws=ws,
87
+ scan_lock=scan_lock,
88
+ seeding_path=seeding_path,
89
+ kind=kind,
90
+ tmdb_id=tmdb_id,
91
+ ):
92
+ et = ev["type"]
93
+ if et == "log":
94
+ print(ev["data"])
95
+ elif et == "progress":
96
+ print(f"[progress] {ev['data']}")
97
+ elif et == "error":
98
+ print(f"[error] {ev['data']}", file=sys.stderr)
99
+ elif et == "done":
100
+ code = ev.get("exit_code", -1)
101
+ finally:
102
+ await ws.stop()
103
+ await client.aclose()
104
+ return code
105
+
106
+ try:
107
+ return asyncio.run(_drain())
108
+ except KeyboardInterrupt:
109
+ return 130
110
+
111
+
112
+ def handle_file(path: Path):
113
+ if not path.exists() or not path.is_file():
114
+ print(f"Errore: file non valido: {path}")
115
+ sys.exit(1)
116
+
117
+ print(f"Analisi tracce audio: {path.name} ...")
118
+ if not has_italian_audio(path):
119
+ print("Italiano non trovato nelle tracce audio. Uscita.")
120
+ sys.exit(1)
121
+
122
+ if not prompt_confirm("Lingua italiana trovata. Proseguo con hardlink e rinomina? [y/n]: "):
123
+ print("Annullato.")
124
+ sys.exit(0)
125
+
126
+ from guessit import guessit
127
+ guess = dict(guessit(path.name))
128
+ title_hint = guess.get("title", "")
129
+ kind, tmdb_data = ask_tmdb_id("movie", title_hint)
130
+ title = tmdb_data.get("title") or tmdb_data.get("name") or title_hint
131
+ year = tmdb_year(tmdb_data, kind)
132
+
133
+ specs = extract_specs(path)
134
+ source, src_type = map_source(guess)
135
+ tag = guess.get("release_group", "") or ""
136
+
137
+ proposed = build_name(
138
+ title=title, year=year, se="",
139
+ specs=specs, source=source, src_type=src_type, tag=tag,
140
+ cut="", repack="REPACK" if guess.get("proper_count") else "",
141
+ )
142
+ final_name = prompt_edit("Nome finale: ", proposed)
143
+ if not final_name:
144
+ print("Nome vuoto, annullato.")
145
+ sys.exit(1)
146
+
147
+ target = do_hardlink_movie(path, final_name)
148
+ print(f"Hardlink creato: {target}")
149
+
150
+ if not prompt_confirm(f"Uploadare '{target.name}' tramite Unit3DWebUp? [y/n]:"):
151
+ print("Annullato (hardlink rimane in ~/seedings).")
152
+ sys.exit(0)
153
+
154
+ sys.exit(run_webup_sync(str(target.resolve()), kind="movie"))
155
+
156
+
157
+ def handle_folder(folder: Path):
158
+ if not folder.exists() or not folder.is_dir():
159
+ print(f"Errore: cartella non valida: {folder}")
160
+ sys.exit(1)
161
+
162
+ files = list(iter_video_files(folder))
163
+ if not files:
164
+ print("Nessun file video trovato.")
165
+ sys.exit(1)
166
+
167
+ print(f"Trovati {len(files)} file video. Analisi tracce audio ...")
168
+ for f in files:
169
+ print(f" {f.relative_to(folder)} ... ", end="", flush=True)
170
+ if has_italian_audio(f):
171
+ print("ok")
172
+ else:
173
+ print("NO ITALIANO")
174
+ print(f"\nFile senza traccia italiana: {f}")
175
+ sys.exit(1)
176
+
177
+ if not prompt_confirm("\nItaliano trovato in tutti i file. Proseguo con hardlink e rinomina? [y/n]: "):
178
+ print("Annullato.")
179
+ sys.exit(0)
180
+
181
+ from guessit import guessit
182
+ folder_guess = dict(guessit(folder.name))
183
+ title_hint = folder_guess.get("title", folder.name)
184
+ kind, tmdb_data = ask_tmdb_id("tv", title_hint)
185
+ series_title = tmdb_data.get("name") or tmdb_data.get("title") or title_hint
186
+ year = tmdb_year(tmdb_data, kind)
187
+
188
+ episode_rename: dict[Path, str] = {}
189
+ sample_specs = None
190
+ sample_source = ""
191
+ sample_type = ""
192
+ sample_tag = ""
193
+ for f in files:
194
+ g = dict(guessit(f.name))
195
+ season = g.get("season")
196
+ if isinstance(season, list):
197
+ season = season[0]
198
+ episode = g.get("episode")
199
+ se = format_se(season, episode)
200
+ if not se:
201
+ print(f"Avviso: impossibile ricavare S##E## da '{f.name}'. Lo salto.")
202
+ continue
203
+ specs = extract_specs(f)
204
+ source, src_type = map_source(g)
205
+ tag = g.get("release_group", "") or folder_guess.get("release_group", "") or ""
206
+ if sample_specs is None:
207
+ sample_specs, sample_source, sample_type, sample_tag = specs, source, src_type, tag
208
+ new_name = build_name(
209
+ title=series_title, year="", se=se,
210
+ specs=specs, source=source, src_type=src_type, tag=tag,
211
+ )
212
+ episode_rename[f] = new_name
213
+
214
+ folder_name = build_name(
215
+ title=series_title, year="", se="",
216
+ specs=sample_specs or {}, source=sample_source, src_type=sample_type, tag=sample_tag,
217
+ )
218
+ folder_name = prompt_edit("Nome cartella finale: ", folder_name)
219
+ if not folder_name:
220
+ print("Nome vuoto, annullato.")
221
+ sys.exit(1)
222
+
223
+ target_dir = do_hardlink_series(folder, folder_name, episode_rename)
224
+ print(f"Hardlink creati in: {target_dir}")
225
+ for orig, new in episode_rename.items():
226
+ print(f" {orig.name} -> {new}{orig.suffix.lower()}")
227
+
228
+ if not prompt_confirm(f"Uploadare '{target_dir.name}' tramite Unit3DWebUp? [y/n]: "):
229
+ print("Annullato (hardlink rimane in ~/seedings).")
230
+ sys.exit(0)
231
+
232
+ sys.exit(run_webup_sync(str(target_dir.resolve()), kind="series"))
233
+
234
+
235
+ def main():
236
+ import argparse
237
+ parser = argparse.ArgumentParser(
238
+ description="Verifica lingua italiana, rinomina secondo nomenclatura ItaTorrents e carica tramite Unit3DWebUp."
239
+ )
240
+ group = parser.add_mutually_exclusive_group(required=True)
241
+ group.add_argument("-u", "--upload", metavar="FILE", help="Singolo file video (film)")
242
+ group.add_argument("-f", "--folder", metavar="CARTELLA", help="Cartella (serie TV)")
243
+ args = parser.parse_args()
244
+
245
+ if args.upload:
246
+ handle_file(Path(args.upload).expanduser().resolve())
247
+ else:
248
+ handle_folder(Path(args.folder).expanduser().resolve())
249
+
250
+
251
+ if __name__ == "__main__":
252
+ try:
253
+ main()
254
+ except KeyboardInterrupt:
255
+ print("\nInterrotto. Ciao!")
256
+ sys.exit(130)