normalizar-case

This commit is contained in:
2026-02-20 17:37:12 +01:00
parent 8d83d27bd3
commit ab056c342e
7 changed files with 149 additions and 1 deletions
+1
View File
@@ -1,5 +1,6 @@
backup/
CLAUDE.md
.claude/
# ---> Python
# Byte-compiled / optimized / DLL files
+1 -1
View File
@@ -1,6 +1,6 @@
# core/constants.py
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
TRASH_FILES = {
"thumbs.db",
+47
View File
@@ -20,6 +20,7 @@ from processors.checks import (
check_comicinfo,
check_foreign,
check_nested,
check_extension_case,
)
from processors.page_normalizer import normalize_pages, preview_normalize_pages
from processors.image_normalizer import (
@@ -28,6 +29,7 @@ from processors.image_normalizer import (
uniformize_images,
preview_uniformize_images,
)
from processors.case_normalizer import normalize_case, preview_normalize_case
class Pipeline:
@@ -38,12 +40,14 @@ class Pipeline:
desired_image_format: str = ".jpg",
collision_policy: str = CollisionPolicy.ABORT,
dry_run: bool = False,
case_mode: str = "lower",
):
self.steps = steps
self.desired_format = desired_format
self.desired_image_format = desired_image_format
self.collision_policy = collision_policy
self.dry_run = dry_run
self.case_mode = case_mode
def _compute_preview(self, step: str, temp_dir: str, step_results: list) -> dict:
if step == "clean":
@@ -82,6 +86,10 @@ class Pipeline:
conversions = preview_normalize_images(temp_dir, 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":
return {"target_format": self.desired_format.upper()}
@@ -91,6 +99,10 @@ class Pipeline:
for step in self.steps:
if step in ("normalize_pages", "normalize_images", "convert_images"):
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 needs_conversion(real_format, self.desired_format):
return True
@@ -134,8 +146,34 @@ class Pipeline:
check_comicinfo(names),
check_foreign(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
if not self._needs_extraction(step_results, real_format, path):
return ComicResult(original_path=path, final_path=path, steps=step_results)
@@ -198,6 +236,15 @@ class Pipeline:
if img_result.changed:
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:
preview = self._compute_preview("convert", temp_dir, step_results)
if confirm_fn is None or confirm_fn("convert", preview):
+4
View File
@@ -58,6 +58,7 @@ class SummaryCollector:
pages_normalized = count_step(["normalize_pages"])
images_converted = count_step(["normalize_images", "convert_images"])
format_converted = count_step(["convert"])
case_normalized = count_step(["normalize_case", "normalize_case_outer"])
lines = [
_BORDER,
@@ -76,6 +77,8 @@ class SummaryCollector:
lines.append(f" · Imágenes convertidas : {images_converted:>3}")
if format_converted:
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" Errores : {len(errors):>3}")
if errors:
@@ -137,6 +140,7 @@ class SummaryCollector:
("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"),
("Sin ComicInfo.xml", "check_comicinfo", lambda w: True, [], None),
("Case de extensión", "check_extension_case", lambda w: True, ["normalize_case"], "normalizado"),
]
output = []
for label, step_name, predicate, resolver_steps, fix_label in categories:
+24
View File
@@ -70,6 +70,20 @@ def _print_preview(step: str, preview: dict, formato: str) -> None:
print(f" ... y {n - 10} más")
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"):
conversions = preview["conversions"]
target_ext = preview["target_ext"].lstrip(".")
@@ -117,6 +131,13 @@ def parse_args():
parser.add_argument("--uniformizar-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("--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(
@@ -171,6 +192,8 @@ def main():
steps.append("normalize_images")
if args.convertir_imagenes:
steps.append("convert_images")
if args.normalizar_case or args.estandarizar:
steps.append("normalize_case")
if args.convertir or args.estandarizar:
steps.append("convert")
@@ -182,6 +205,7 @@ def main():
desired_format=args.formato,
desired_image_format="." + args.formato_imagen,
collision_policy=collision,
case_mode=args.modo_case,
)
collector = SummaryCollector()
try:
+47
View File
@@ -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)
+25
View File
@@ -155,3 +155,28 @@ def check_comicinfo(names: list[str]) -> StepResult:
)
warnings = [] if found else ["Falta ComicInfo.xml"]
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)