normalizar-case
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
backup/
|
backup/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
.claude/
|
||||||
|
|
||||||
# ---> Python
|
# ---> Python
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# core/constants.py
|
# core/constants.py
|
||||||
|
|
||||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
|
||||||
|
|
||||||
TRASH_FILES = {
|
TRASH_FILES = {
|
||||||
"thumbs.db",
|
"thumbs.db",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from processors.checks import (
|
|||||||
check_comicinfo,
|
check_comicinfo,
|
||||||
check_foreign,
|
check_foreign,
|
||||||
check_nested,
|
check_nested,
|
||||||
|
check_extension_case,
|
||||||
)
|
)
|
||||||
from processors.page_normalizer import normalize_pages, preview_normalize_pages
|
from processors.page_normalizer import normalize_pages, preview_normalize_pages
|
||||||
from processors.image_normalizer import (
|
from processors.image_normalizer import (
|
||||||
@@ -28,6 +29,7 @@ from processors.image_normalizer import (
|
|||||||
uniformize_images,
|
uniformize_images,
|
||||||
preview_uniformize_images,
|
preview_uniformize_images,
|
||||||
)
|
)
|
||||||
|
from processors.case_normalizer import normalize_case, preview_normalize_case
|
||||||
|
|
||||||
|
|
||||||
class Pipeline:
|
class Pipeline:
|
||||||
@@ -38,12 +40,14 @@ class Pipeline:
|
|||||||
desired_image_format: str = ".jpg",
|
desired_image_format: str = ".jpg",
|
||||||
collision_policy: str = CollisionPolicy.ABORT,
|
collision_policy: str = CollisionPolicy.ABORT,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
|
case_mode: str = "lower",
|
||||||
):
|
):
|
||||||
self.steps = steps
|
self.steps = steps
|
||||||
self.desired_format = desired_format
|
self.desired_format = desired_format
|
||||||
self.desired_image_format = desired_image_format
|
self.desired_image_format = desired_image_format
|
||||||
self.collision_policy = collision_policy
|
self.collision_policy = collision_policy
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
|
self.case_mode = case_mode
|
||||||
|
|
||||||
def _compute_preview(self, step: str, temp_dir: str, step_results: list) -> dict:
|
def _compute_preview(self, step: str, temp_dir: str, step_results: list) -> dict:
|
||||||
if step == "clean":
|
if step == "clean":
|
||||||
@@ -82,6 +86,10 @@ class Pipeline:
|
|||||||
conversions = preview_normalize_images(temp_dir, self.desired_image_format)
|
conversions = preview_normalize_images(temp_dir, self.desired_image_format)
|
||||||
return {"conversions": conversions, "target_ext": self.desired_image_format}
|
return {"conversions": conversions, "target_ext": self.desired_image_format}
|
||||||
|
|
||||||
|
elif step == "normalize_case":
|
||||||
|
renames = preview_normalize_case(temp_dir, self.case_mode)
|
||||||
|
return {"renames": renames, "mode": self.case_mode}
|
||||||
|
|
||||||
elif step == "convert":
|
elif step == "convert":
|
||||||
return {"target_format": self.desired_format.upper()}
|
return {"target_format": self.desired_format.upper()}
|
||||||
|
|
||||||
@@ -91,6 +99,10 @@ class Pipeline:
|
|||||||
for step in self.steps:
|
for step in self.steps:
|
||||||
if step in ("normalize_pages", "normalize_images", "convert_images"):
|
if step in ("normalize_pages", "normalize_images", "convert_images"):
|
||||||
return True
|
return True
|
||||||
|
if step == "normalize_case":
|
||||||
|
case_r = next((r for r in step_results if r.step == "check_extension_case"), None)
|
||||||
|
if case_r and case_r.warnings:
|
||||||
|
return True
|
||||||
if step == "convert":
|
if step == "convert":
|
||||||
if needs_conversion(real_format, self.desired_format):
|
if needs_conversion(real_format, self.desired_format):
|
||||||
return True
|
return True
|
||||||
@@ -134,8 +146,34 @@ class Pipeline:
|
|||||||
check_comicinfo(names),
|
check_comicinfo(names),
|
||||||
check_foreign(names),
|
check_foreign(names),
|
||||||
check_nested(names),
|
check_nested(names),
|
||||||
|
check_extension_case(names, self.case_mode),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 3b. Corrección del case de la extensión exterior (renombrado en-sitio, sin repack)
|
||||||
|
if "normalize_case" in self.steps:
|
||||||
|
outer_ext = os.path.splitext(path)[1]
|
||||||
|
target_outer_ext = outer_ext.lower() if self.case_mode == "lower" else outer_ext.upper()
|
||||||
|
if outer_ext != target_outer_ext:
|
||||||
|
base_no_ext = os.path.splitext(path)[0]
|
||||||
|
new_outer_path = base_no_ext + target_outer_ext
|
||||||
|
try:
|
||||||
|
safe_outer = resolve_collision(new_outer_path, self.collision_policy)
|
||||||
|
if not self.dry_run:
|
||||||
|
os.rename(path, safe_outer)
|
||||||
|
path = safe_outer
|
||||||
|
step_results.append(StepResult(
|
||||||
|
step="normalize_case_outer",
|
||||||
|
changed=True,
|
||||||
|
details=[f"Extensión exterior corregida: {outer_ext} → {target_outer_ext}"],
|
||||||
|
))
|
||||||
|
except (FileExistsError, OSError) as exc:
|
||||||
|
step_results.append(StepResult(
|
||||||
|
step="normalize_case_outer",
|
||||||
|
changed=False,
|
||||||
|
errors=[str(exc)],
|
||||||
|
))
|
||||||
|
return ComicResult(original_path=path, final_path=None, steps=step_results)
|
||||||
|
|
||||||
# 4. Pre-flight: si ningún step necesita extracción, salir sin tocar el archivo
|
# 4. Pre-flight: si ningún step necesita extracción, salir sin tocar el archivo
|
||||||
if not self._needs_extraction(step_results, real_format, path):
|
if not self._needs_extraction(step_results, real_format, path):
|
||||||
return ComicResult(original_path=path, final_path=path, steps=step_results)
|
return ComicResult(original_path=path, final_path=path, steps=step_results)
|
||||||
@@ -198,6 +236,15 @@ class Pipeline:
|
|||||||
if img_result.changed:
|
if img_result.changed:
|
||||||
any_changed = True
|
any_changed = True
|
||||||
|
|
||||||
|
if "normalize_case" in self.steps:
|
||||||
|
preview = self._compute_preview("normalize_case", temp_dir, step_results)
|
||||||
|
if preview.get("renames"):
|
||||||
|
if confirm_fn is None or confirm_fn("normalize_case", preview):
|
||||||
|
case_result = normalize_case(temp_dir, self.case_mode)
|
||||||
|
step_results.append(case_result)
|
||||||
|
if case_result.changed:
|
||||||
|
any_changed = True
|
||||||
|
|
||||||
if "convert" in self.steps:
|
if "convert" in self.steps:
|
||||||
preview = self._compute_preview("convert", temp_dir, step_results)
|
preview = self._compute_preview("convert", temp_dir, step_results)
|
||||||
if confirm_fn is None or confirm_fn("convert", preview):
|
if confirm_fn is None or confirm_fn("convert", preview):
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class SummaryCollector:
|
|||||||
pages_normalized = count_step(["normalize_pages"])
|
pages_normalized = count_step(["normalize_pages"])
|
||||||
images_converted = count_step(["normalize_images", "convert_images"])
|
images_converted = count_step(["normalize_images", "convert_images"])
|
||||||
format_converted = count_step(["convert"])
|
format_converted = count_step(["convert"])
|
||||||
|
case_normalized = count_step(["normalize_case", "normalize_case_outer"])
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
_BORDER,
|
_BORDER,
|
||||||
@@ -76,6 +77,8 @@ class SummaryCollector:
|
|||||||
lines.append(f" · Imágenes convertidas : {images_converted:>3}")
|
lines.append(f" · Imágenes convertidas : {images_converted:>3}")
|
||||||
if format_converted:
|
if format_converted:
|
||||||
lines.append(f" · Formato convertido : {format_converted:>3}")
|
lines.append(f" · Formato convertido : {format_converted:>3}")
|
||||||
|
if case_normalized:
|
||||||
|
lines.append(f" · Case normalizado : {case_normalized:>3}")
|
||||||
lines.append(f" Advertencias : {len(warnings_only):>3}")
|
lines.append(f" Advertencias : {len(warnings_only):>3}")
|
||||||
lines.append(f" Errores : {len(errors):>3}")
|
lines.append(f" Errores : {len(errors):>3}")
|
||||||
if errors:
|
if errors:
|
||||||
@@ -137,6 +140,7 @@ class SummaryCollector:
|
|||||||
("Numeración de páginas", "check_page_numbering", lambda w: True, ["normalize_pages"], "renumerado"),
|
("Numeración de páginas", "check_page_numbering", lambda w: True, ["normalize_pages"], "renumerado"),
|
||||||
("Imágenes mezcladas", "check_image_extensions", lambda w: True, ["normalize_images", "convert_images"], "normalizado"),
|
("Imágenes mezcladas", "check_image_extensions", lambda w: True, ["normalize_images", "convert_images"], "normalizado"),
|
||||||
("Sin ComicInfo.xml", "check_comicinfo", lambda w: True, [], None),
|
("Sin ComicInfo.xml", "check_comicinfo", lambda w: True, [], None),
|
||||||
|
("Case de extensión", "check_extension_case", lambda w: True, ["normalize_case"], "normalizado"),
|
||||||
]
|
]
|
||||||
output = []
|
output = []
|
||||||
for label, step_name, predicate, resolver_steps, fix_label in categories:
|
for label, step_name, predicate, resolver_steps, fix_label in categories:
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ def _print_preview(step: str, preview: dict, formato: str) -> None:
|
|||||||
print(f" ... y {n - 10} más")
|
print(f" ... y {n - 10} más")
|
||||||
print(f"Formato final del archivo: {fmt}")
|
print(f"Formato final del archivo: {fmt}")
|
||||||
|
|
||||||
|
elif step == "normalize_case":
|
||||||
|
renames = preview["renames"]
|
||||||
|
mode_label = "minúsculas" if preview["mode"] == "lower" else "mayúsculas"
|
||||||
|
n = len(renames)
|
||||||
|
print(f"Normalización de extensiones a {mode_label} ({n} ficheros):")
|
||||||
|
print(f" {'Nombre actual':<{_COL_W}} → Nombre final")
|
||||||
|
print(f" {_SEP}")
|
||||||
|
display = renames[:10] if n > 20 else renames
|
||||||
|
for orig, final in display:
|
||||||
|
print(f" {orig:<{_COL_W}} {final}")
|
||||||
|
if n > 20:
|
||||||
|
print(f" ... y {n - 10} más")
|
||||||
|
print(f"Formato final del archivo: {formato.upper()}")
|
||||||
|
|
||||||
elif step in ("normalize_images", "convert_images"):
|
elif step in ("normalize_images", "convert_images"):
|
||||||
conversions = preview["conversions"]
|
conversions = preview["conversions"]
|
||||||
target_ext = preview["target_ext"].lstrip(".")
|
target_ext = preview["target_ext"].lstrip(".")
|
||||||
@@ -117,6 +131,13 @@ def parse_args():
|
|||||||
parser.add_argument("--uniformizar-imagenes", action="store_true")
|
parser.add_argument("--uniformizar-imagenes", action="store_true")
|
||||||
parser.add_argument("--convertir-imagenes", action="store_true")
|
parser.add_argument("--convertir-imagenes", action="store_true")
|
||||||
parser.add_argument("--formato-imagen", choices=["jpg", "png", "webp"], default="png")
|
parser.add_argument("--formato-imagen", choices=["jpg", "png", "webp"], default="png")
|
||||||
|
parser.add_argument("--normalizar-case", action="store_true", dest="normalizar_case")
|
||||||
|
parser.add_argument(
|
||||||
|
"--modo-case",
|
||||||
|
choices=["lower", "upper"],
|
||||||
|
default="lower",
|
||||||
|
dest="modo_case",
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument("--no-preguntar", action="store_true")
|
parser.add_argument("--no-preguntar", action="store_true")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -171,6 +192,8 @@ def main():
|
|||||||
steps.append("normalize_images")
|
steps.append("normalize_images")
|
||||||
if args.convertir_imagenes:
|
if args.convertir_imagenes:
|
||||||
steps.append("convert_images")
|
steps.append("convert_images")
|
||||||
|
if args.normalizar_case or args.estandarizar:
|
||||||
|
steps.append("normalize_case")
|
||||||
if args.convertir or args.estandarizar:
|
if args.convertir or args.estandarizar:
|
||||||
steps.append("convert")
|
steps.append("convert")
|
||||||
|
|
||||||
@@ -182,6 +205,7 @@ def main():
|
|||||||
desired_format=args.formato,
|
desired_format=args.formato,
|
||||||
desired_image_format="." + args.formato_imagen,
|
desired_image_format="." + args.formato_imagen,
|
||||||
collision_policy=collision,
|
collision_policy=collision,
|
||||||
|
case_mode=args.modo_case,
|
||||||
)
|
)
|
||||||
collector = SummaryCollector()
|
collector = SummaryCollector()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# processors/case_normalizer.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from core.constants import IMAGE_EXTENSIONS
|
||||||
|
from core.result import StepResult
|
||||||
|
|
||||||
|
|
||||||
|
def preview_normalize_case(work_dir: str, mode: str = "lower") -> list[tuple[str, str]]:
|
||||||
|
"""Devuelve lista de (nombre_actual, nombre_final) sin modificar nada."""
|
||||||
|
result = []
|
||||||
|
for root, _, files in os.walk(work_dir):
|
||||||
|
for f in files:
|
||||||
|
stem, ext = os.path.splitext(f)
|
||||||
|
if not ext or ext.lower() not in IMAGE_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
target_ext = ext.lower() if mode == "lower" else ext.upper()
|
||||||
|
if ext != target_ext:
|
||||||
|
result.append((f, stem + target_ext))
|
||||||
|
return sorted(result)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_case(work_dir: str, mode: str = "lower") -> StepResult:
|
||||||
|
"""
|
||||||
|
Renombra las extensiones de las imágenes en work_dir al case indicado.
|
||||||
|
Solo afecta a extensiones de imagen (IMAGE_EXTENSIONS); ComicInfo.xml y
|
||||||
|
otros ficheros no de imagen quedan intactos.
|
||||||
|
En Linux (FS sensible al case) el rename directo es seguro.
|
||||||
|
"""
|
||||||
|
changed = False
|
||||||
|
details = []
|
||||||
|
|
||||||
|
for root, _, files in os.walk(work_dir):
|
||||||
|
for f in files:
|
||||||
|
stem, ext = os.path.splitext(f)
|
||||||
|
if not ext or ext.lower() not in IMAGE_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
target_ext = ext.lower() if mode == "lower" else ext.upper()
|
||||||
|
if ext == target_ext:
|
||||||
|
continue
|
||||||
|
src = os.path.join(root, f)
|
||||||
|
dst = os.path.join(root, stem + target_ext)
|
||||||
|
os.rename(src, dst)
|
||||||
|
details.append(f"{f} → {stem + target_ext}")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
return StepResult(step="normalize_case", changed=changed, details=details)
|
||||||
@@ -155,3 +155,28 @@ def check_comicinfo(names: list[str]) -> StepResult:
|
|||||||
)
|
)
|
||||||
warnings = [] if found else ["Falta ComicInfo.xml"]
|
warnings = [] if found else ["Falta ComicInfo.xml"]
|
||||||
return StepResult(step="check_comicinfo", changed=False, warnings=warnings)
|
return StepResult(step="check_comicinfo", changed=False, warnings=warnings)
|
||||||
|
|
||||||
|
|
||||||
|
def check_extension_case(names: list[str], mode: str = "lower") -> StepResult:
|
||||||
|
"""Detecta imágenes cuya extensión no está en el case esperado (lower/upper)."""
|
||||||
|
mismatches = []
|
||||||
|
for name in names:
|
||||||
|
normalized = name.replace("\\", "/")
|
||||||
|
if normalized.endswith("/"):
|
||||||
|
continue
|
||||||
|
basename = normalized.rsplit("/", 1)[-1]
|
||||||
|
_, ext = os.path.splitext(basename)
|
||||||
|
if not ext or ext.lower() not in IMAGE_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
target_ext = ext.lower() if mode == "lower" else ext.upper()
|
||||||
|
if ext != target_ext:
|
||||||
|
mismatches.append(basename)
|
||||||
|
|
||||||
|
if not mismatches:
|
||||||
|
warnings = []
|
||||||
|
elif len(mismatches) <= 3:
|
||||||
|
warnings = [f"Case incorrecto en extensión: {f}" for f in sorted(mismatches)]
|
||||||
|
else:
|
||||||
|
warnings = [f"Case incorrecto en extensión: {len(mismatches)} ficheros"]
|
||||||
|
|
||||||
|
return StepResult(step="check_extension_case", changed=False, warnings=warnings)
|
||||||
|
|||||||
Reference in New Issue
Block a user