normalizar-case
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
backup/
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# core/constants.py
|
||||
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
|
||||
|
||||
TRASH_FILES = {
|
||||
"thumbs.db",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
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