bhtool 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {bhtool-0.2.3 → bhtool-0.2.4}/PKG-INFO +1 -1
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/list_movies.py +44 -15
- {bhtool-0.2.3 → bhtool-0.2.4}/pyproject.toml +1 -1
- {bhtool-0.2.3 → bhtool-0.2.4}/.gitignore +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/.python-version +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/README.md +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/bumpver.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/cli.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/clr_chmod.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/npread.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/portkill.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/psword.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/rm_npm.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/run.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/text.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/todict.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/bhtool/utils.py +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/jtools/jtools.js +0 -0
- {bhtool-0.2.3 → bhtool-0.2.4}/jtools/package.json +0 -0
|
@@ -26,10 +26,6 @@ _MOVIES_BORDER = "cyan"
|
|
|
26
26
|
_MOVIES_TITLE = "movies"
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _movies_console() -> Console:
|
|
30
|
-
return Console(highlight=False)
|
|
31
|
-
|
|
32
|
-
|
|
33
29
|
def _panel_lines(body: Text | str, *, title: str = _MOVIES_TITLE, border: str = _MOVIES_BORDER) -> Panel:
|
|
34
30
|
return Panel(
|
|
35
31
|
body,
|
|
@@ -41,6 +37,32 @@ def _panel_lines(body: Text | str, *, title: str = _MOVIES_TITLE, border: str =
|
|
|
41
37
|
)
|
|
42
38
|
|
|
43
39
|
|
|
40
|
+
def _is_case_only_rename(old_path: Path, new_path: Path) -> bool:
|
|
41
|
+
"""True when old_path and new_path are the same file differing only in case.
|
|
42
|
+
|
|
43
|
+
On case-insensitive filesystems (macOS, Windows) new_path.exists() is True
|
|
44
|
+
for a pure capitalization change, since it resolves to the same file.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
return old_path.samefile(new_path)
|
|
48
|
+
except OSError:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _apply_rename(old_path: Path, new_path: Path) -> None:
|
|
53
|
+
"""Rename, forcing a case change on case-insensitive filesystems."""
|
|
54
|
+
try:
|
|
55
|
+
old_path.rename(new_path)
|
|
56
|
+
except OSError:
|
|
57
|
+
tmp_path = old_path.with_name(old_path.name + ".bhtool-tmp")
|
|
58
|
+
old_path.rename(tmp_path)
|
|
59
|
+
try:
|
|
60
|
+
tmp_path.rename(new_path)
|
|
61
|
+
except OSError:
|
|
62
|
+
tmp_path.rename(old_path)
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
|
|
44
66
|
def _status_text(status: str) -> Text:
|
|
45
67
|
if status == "would rename":
|
|
46
68
|
return Text(status, style="green")
|
|
@@ -53,7 +75,10 @@ NORMALIZE_SYSTEM_PROMPT = textwrap.dedent("""
|
|
|
53
75
|
You normalize movie and TV directory and file names for a media library.
|
|
54
76
|
|
|
55
77
|
Rules:
|
|
56
|
-
- Output the title in Title Case
|
|
78
|
+
- Output the title in Title Case: capitalize the first and last words and
|
|
79
|
+
all principal words, but keep articles, conjunctions and short
|
|
80
|
+
prepositions (a, an, the, and, or, of, in, on, to, for) lowercase
|
|
81
|
+
unless they are the first or last word (e.g. "The Lord of the Rings").
|
|
57
82
|
- Keep the year as (YYYY) at the end when present; infer from the name
|
|
58
83
|
if needed.
|
|
59
84
|
- For TV series: preserve season markers at the end if present (e.g. S1,
|
|
@@ -112,7 +137,7 @@ async def normalize_names_with_llm(
|
|
|
112
137
|
if not dir_names and not file_names:
|
|
113
138
|
return {"directories": [], "files": []}
|
|
114
139
|
|
|
115
|
-
console = console or
|
|
140
|
+
console = console or Console()
|
|
116
141
|
dir_list = "\n".join(f"- {n}" for n in dir_names) or "(none)"
|
|
117
142
|
file_list = "\n".join(f"- {n}" for n in file_names) or "(none)"
|
|
118
143
|
user_content = NORMALIZE_USER_PROMPT_TEMPLATE.format(
|
|
@@ -127,13 +152,17 @@ async def normalize_names_with_llm(
|
|
|
127
152
|
try:
|
|
128
153
|
async with get_llm_client(service) as client:
|
|
129
154
|
llm_body = Text.assemble(
|
|
130
|
-
(
|
|
155
|
+
("service ", ""),
|
|
156
|
+
(client.service, "bold"),
|
|
157
|
+
(" model ", ""),
|
|
158
|
+
(client.model, "bold"),
|
|
159
|
+
("\n", ""),
|
|
131
160
|
("Requesting normalized names from the LLM…", "dim"),
|
|
132
161
|
)
|
|
133
162
|
console.print(_panel_lines(llm_body, title="LLM", border="blue"))
|
|
134
163
|
result = await client.get_completion(messages)
|
|
135
164
|
except Exception as e:
|
|
136
|
-
err =
|
|
165
|
+
err = Console(stderr=True)
|
|
137
166
|
err.print(CycloptsPanel(str(e), title="Error", style="red"))
|
|
138
167
|
err.print(
|
|
139
168
|
CycloptsPanel(
|
|
@@ -154,7 +183,7 @@ async def normalize_names_with_llm(
|
|
|
154
183
|
|
|
155
184
|
|
|
156
185
|
def run_normalize_with_llm(root_dir, service="openai", *, console: Console | None = None):
|
|
157
|
-
console = console or
|
|
186
|
+
console = console or Console()
|
|
158
187
|
root = Path(root_dir)
|
|
159
188
|
video_suffixes = {".avi", ".mkv", ".mp4", ".m4v", ".mov", ".wmv", ".webm"}
|
|
160
189
|
skip_names = {
|
|
@@ -202,7 +231,7 @@ def run_normalize_with_llm(root_dir, service="openai", *, console: Console | Non
|
|
|
202
231
|
def rename_movies(
|
|
203
232
|
root_dir, dry_run=True, mapping=None, *, console: Console | None = None
|
|
204
233
|
):
|
|
205
|
-
console = console or
|
|
234
|
+
console = console or Console()
|
|
206
235
|
root = Path(root_dir)
|
|
207
236
|
if mapping is None:
|
|
208
237
|
mapping = {"directories": [], "files": []}
|
|
@@ -223,7 +252,7 @@ def rename_movies(
|
|
|
223
252
|
if table_rows is not None:
|
|
224
253
|
table_rows.append(("DIR", old_name, new_name, "skip (not found)"))
|
|
225
254
|
continue
|
|
226
|
-
if new_path.exists():
|
|
255
|
+
if new_path.exists() and not _is_case_only_rename(old_path, new_path):
|
|
227
256
|
skipped_count += 1
|
|
228
257
|
if table_rows is not None:
|
|
229
258
|
table_rows.append(("DIR", old_name, new_name, "skip (exists)"))
|
|
@@ -231,7 +260,7 @@ def rename_movies(
|
|
|
231
260
|
if dry_run:
|
|
232
261
|
table_rows.append(("DIR", old_name, new_name, "would rename"))
|
|
233
262
|
else:
|
|
234
|
-
old_path
|
|
263
|
+
_apply_rename(old_path, new_path)
|
|
235
264
|
console.print(
|
|
236
265
|
Text.assemble(
|
|
237
266
|
("✅ DIR ", "green"),
|
|
@@ -256,7 +285,7 @@ def rename_movies(
|
|
|
256
285
|
("FILE", old_name, new_name + suffix, "skip (not found)")
|
|
257
286
|
)
|
|
258
287
|
continue
|
|
259
|
-
if new_path.exists():
|
|
288
|
+
if new_path.exists() and not _is_case_only_rename(old_path, new_path):
|
|
260
289
|
skipped_count += 1
|
|
261
290
|
if table_rows is not None:
|
|
262
291
|
table_rows.append(
|
|
@@ -266,7 +295,7 @@ def rename_movies(
|
|
|
266
295
|
if dry_run:
|
|
267
296
|
table_rows.append(("FILE", old_name, new_name + suffix, "would rename"))
|
|
268
297
|
else:
|
|
269
|
-
old_path
|
|
298
|
+
_apply_rename(old_path, new_path)
|
|
270
299
|
console.print(
|
|
271
300
|
Text.assemble(
|
|
272
301
|
("✅ FILE ", "green"),
|
|
@@ -323,7 +352,7 @@ def _root_dir(movies_dir: str | None = None) -> Path:
|
|
|
323
352
|
|
|
324
353
|
|
|
325
354
|
def rename(root_dir: str | None = None, execute: bool = False):
|
|
326
|
-
console =
|
|
355
|
+
console = Console()
|
|
327
356
|
intro = Text.assemble(
|
|
328
357
|
("LLM-normalize names → ", ""),
|
|
329
358
|
("movie_mapping.json", "cyan"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|