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
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)
|