2.2.0: la fecha de verdad manda, sin preguntar nada

- R.fecha_real: la fecha sale de lo más antiguo que sabe el disco. Al copiar en
  Windows la creación pasa a ser la de la copia y la modificación se conserva;
  al renderizar, las dos son nuevas.
- R.base_derivado y _heredar_fechas: un render hereda la fecha del original, en
  la misma carpeta o en otra si el nombre es propio y único. Si hay dos
  candidatos iguales no hereda.
- El plan archiva el material en el mes en que se grabó, no en el que dice la
  carpeta, y explica lo que hizo en vez de pedir una decisión. Los 727 GB de
  las dos carpetas que mentían se resuelven solos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-16 22:01:33 -05:00
padre 1e1187d28a
commit 99d0ba2034
Se han modificado 4 ficheros con 113 adiciones y 26 borrados

Ver fichero

@@ -34,7 +34,7 @@ import actualizador
asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__]
VERSION = "2.1.0"
VERSION = "2.2.0"
CONGELADO = getattr(sys, "frozen", False)
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
@@ -465,7 +465,7 @@ def escanear(ruta):
basura = 1 if (en_basura or rol == "basura") else 0
proxy = 1 if (en_proxy or rol == "proxy") else 0
stock = 1 if (en_stock or rol == "stock") else 0
fecha, origen = R.fecha_de_nombre(nombre, st.st_mtime)
fecha, origen = R.fecha_de_nombre(nombre, R.fecha_real(st))
fy, fm = int(fecha[:4]), int(fecha[5:7])
filas.append((e.path, raiz, path, nombre, ext, st.st_size, st.st_mtime, rol, firma or "",
ctx or "", anio, mes, fecha, origen, basura, proxy, stock))
@@ -487,6 +487,7 @@ def escanear(ruta):
try:
walk(raiz, False, False, False, "", "raiz", None, None)
prog["fallos"] = len(fallos)
_heredar_fechas(filas)
con = db()
# Antes se borraba el inventario y se guardaba lo recogido pasara lo que pasara. Si una unidad de
# red se caía a media lectura, el inventario quedaba truncado y marcado como bueno, y a partir de
@@ -543,6 +544,42 @@ def escanear(ruta):
prog["fin"] = time.time()
def _heredar_fechas(filas):
"""Un render o una conversión se crean mucho después del rodaje, así que su fecha no dice nada del
material: si el original está en la misma carpeta, el derivado toma SU fecha.
Así «C0001_10bit.mp4» se archiva en el mes en que se grabó «C0001.MP4», no en el mes en que se
convirtió, y el mes de una carpeta se decide por el material de verdad."""
I_RUTA, I_DIR, I_NOMBRE, I_FECHA, I_ORIGEN = 0, 2, 3, 12, 13
por_dir = defaultdict(dict)
en_todo = defaultdict(list)
for f in filas:
tallo = os.path.splitext(f[I_NOMBRE])[0].lower()
por_dir[f[I_DIR].lower()][tallo] = f
en_todo[tallo].append(f)
heredadas = 0
for i, f in enumerate(filas):
base = R.base_derivado(f[I_NOMBRE])
if not base:
continue
orig = por_dir[f[I_DIR].lower()].get(base.lower())
if orig is None and len(base) >= 6:
# el render puede estar en otra carpeta («_Exportados», «_Archivo»): vale si el nombre es
# bastante propio y no hay dos candidatos que se llamen igual en toda la carpeta del cliente
cands = en_todo.get(base.lower()) or []
orig = cands[0] if len(cands) == 1 else None
if orig is None or orig is f or orig[I_ORIGEN] == "derivado":
continue
if orig[I_FECHA] >= f[I_FECHA]:
continue # el «original» no es más viejo: no hereda nada
nueva = list(f)
nueva[I_FECHA] = orig[I_FECHA]
nueva[I_ORIGEN] = "derivado" # la fecha viene del archivo del que salió
filas[i] = tuple(nueva)
heredadas += 1
return heredadas
def lanzar_escaneo(ruta):
raiz = os.path.normpath(ruta)
p = PROGRESO.get(raiz)
@@ -810,7 +847,8 @@ class Plan:
if nombre != canon:
motivo = "nombre de mes normalizado" + (" (el número no coincidía con el nombre: gana el nombre)" if conflicto else "")
if discrepa:
motivo += f" · OJO: lo que hay dentro parece de {R.nombre_mes(real[1])}"
motivo += (f" · ojo: lo que hay dentro se grabó en {R.nombre_mes(real[1])}, y se archiva "
f"en ese mes aunque la carpeta se llame así")
self.agregar(e.path, os.path.join(padre, canon), "MES", "renombrar", motivo,
"media" if (conflicto or discrepa) else "alta")
self.plan_dir(e.path, "mes", anio, m)
@@ -961,17 +999,23 @@ class Plan:
elif R.detectar_mes(seg)[0]:
mes_ruta = R.detectar_mes(seg)[0]
if anio and mes:
# La carpeta dice un mes, pero los archivos pueden decir otro. Medido el 16-sep-2026 sobre
# el material real: 5 de las 10 carpetas de mes con material fechable no contenían el mes
# que anunciaban (una llamada «02-marzo» tenía 119 clips de diciembre). Antes esto se movía
# con confianza alta y sin aviso; ahora se avisa y baja a media.
# La carpeta dice un mes y el archivo puede decir otro. Medido el 16-sep-2026 sobre el
# material real: 5 de las 10 carpetas de mes con material fechable no contenían el mes que
# anunciaban (una llamada «02-marzo» tenía 119 clips de diciembre de 2025).
#
# Manda el archivo, no la carpeta: el nombre lo pone la cámara y la fecha de creación la
# pone el disco, mientras que el nombre de la carpeta lo escribió una persona con prisa.
# Así no hay que preguntarle a nadie cuál es el mes bueno.
ay, am, fconf = anio, mes, "alta"
if _discrepa_mes(f["fecha"], f["fecha_origen"], anio, mes):
fconf = "media"
desacuerdo[(int(f["fecha"][:4]), int(f["fecha"][5:7]))] += 1
ra, rm = int(f["fecha"][:4]), int(f["fecha"][5:7])
if f["fecha_origen"] == "nombre+mtime":
ra = anio if rm <= mes else anio - 1 # el nombre de la cámara no trae año
ay, am, fconf = ra, rm, "media"
desacuerdo[(ra, rm)] += 1
else:
ay, am = int(f["fecha"][:4]), int(f["fecha"][5:7])
por_nombre = f["fecha_origen"] != "mtime"
por_nombre = f["fecha_origen"] != "mtime" # «nombre», «nombre+mtime» o «derivado»
fconf = "media" if por_nombre else "baja"
# el año de una carpeta intermedia ("…/2023/…") manda sobre la fecha de modificación,
# pero no sobre una fecha escrita en el nombre del archivo
@@ -994,10 +1038,11 @@ class Plan:
n_fechables = sum(1 for f in filas if not f["es_basura"] and not f["es_proxy"] and f["fecha_origen"] != "mtime")
if desacuerdo and sum(desacuerdo.values()) >= 3 and sum(desacuerdo.values()) >= 0.25 * max(1, n_fechables):
(da, dm), cuantos = desacuerdo.most_common(1)[0]
aviso_fecha = (f" · OJO: la carpeta dice {R.nombre_mes(mes)} pero {cuantos} de sus archivos son de "
f"{R.nombre_mes(dm)} según su propio nombre; comprueba antes de aplicar")
self.avisos.append(f"«{self.rel(e.path)}» dice {R.nombre_mes(mes)} de {anio} y el nombre de {cuantos} de "
f"sus archivos dice {R.nombre_mes(dm)}: se propone sin marcar para que lo revises.")
aviso_fecha = (f" · la carpeta decía {R.nombre_mes(mes)}, pero {cuantos} de sus archivos son de "
f"{R.nombre_mes(dm)} de {da}: van a su mes de verdad")
self.avisos.append(f"«{self.rel(e.path)}» está dentro de {R.nombre_mes(mes)} de {anio}, pero {cuantos} de "
f"sus archivos se grabaron en {R.nombre_mes(dm)} de {da}. Se archivan en el mes en que "
f"se grabaron, no en el que dice la carpeta.")
for (ay, am, fuente, fconf), archivos in grupos.items():
sub = fuente if not etiqueta else os.path.join(etiqueta, fuente)
destino = destino_plantilla(self.tipo, "GRABACION", anio=ay, mes=am, sub=sub, nombre=limpiar_nombre(nombre))
@@ -1005,9 +1050,8 @@ class Plan:
if all(os.path.normpath(os.path.dirname(a[0])).lower() == self.abs_(destino).lower() for a in archivos):
continue
mot = motivo + (" · repartido por fuente" if anio and mes else " · mes por la fecha de cada archivo" + (" (fecha de modificación)" if fconf == "baja" else ""))
marcado = False if aviso_fecha else None
self.agregar(e.path, self.abs_(destino), "GRABACION", "mover archivos", mot + aviso_fecha, fconf,
archivos=archivos, marcado=marcado)
archivos=archivos)
def plan_archivo(self, e, nivel, anio, mes, padre):
fila = self.con.execute("SELECT * FROM archivos WHERE ruta = ?", (e.path,)).fetchone()