Inventario fiel al disco: mayúsculas de las carpetas, mover durante el escaneo, y no convertir dos veces
Cotejado archivo por archivo cada disco con el inventario: el escaneo estaba completo, pero el inventario apuntaba la carpeta con las mayúsculas del plan (1.039 fotos en Vía Sumapaz, tres carpetas donde hay una), un proxy apartado en pleno escaneo quedó dos veces, y los 11 verificados de Línea Gene se habían vuelto a encolar con la 3.7. Ahora la carpeta se escribe como en el disco y se recuenta donde quedó al final, el escaneo vuelve a recorrer si la app movió algo mientras, y al reenganchar se quita la conversión que sobra. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
147
organizador.py
147
organizador.py
@@ -615,18 +615,31 @@ def escanear(ruta):
|
||||
continue
|
||||
|
||||
try:
|
||||
walk(raiz, False, False, False, "", "raiz", None, None)
|
||||
prog["fallos"] = len(fallos)
|
||||
_fechas_de_rodaje(filas)
|
||||
_heredar_fechas(filas)
|
||||
# de qué mes es cada carpeta: se cuenta al final, con las fechas ya corregidas, y sin la basura
|
||||
# ni los proxies (un .DS_Store de otro año no dice nada del material)
|
||||
for f in filas:
|
||||
if f[14] or f[15]:
|
||||
continue
|
||||
am = (int(f[12][:4]), int(f[12][5:7]))
|
||||
for d in subir(f[2]):
|
||||
dirs[d][2][am] += 1
|
||||
# Si mientras se recorre la carpeta la app mueve algo de ella («Aplicar», apartar, la cola), lo recogido
|
||||
# mezcla el antes y el después: un archivo visto en su sitio y luego otra vez en la papelera, o en
|
||||
# ninguno. Medido el 14-oct-2026 en Vía Sumapaz: un proxy apartado a las 10:02, en pleno escaneo, quedó
|
||||
# dos veces en el inventario. Entonces se vuelve a recorrer (hasta tres veces).
|
||||
for vuelta in range(3):
|
||||
movimientos_antes = _TOCADO.get(raiz.lower(), 0)
|
||||
walk(raiz, False, False, False, "", "raiz", None, None)
|
||||
prog["fallos"] = len(fallos)
|
||||
_fechas_de_rodaje(filas)
|
||||
_heredar_fechas(filas)
|
||||
# de qué mes es cada carpeta: se cuenta al final, con las fechas ya corregidas, y sin la basura
|
||||
# ni los proxies (un .DS_Store de otro año no dice nada del material)
|
||||
for f in filas:
|
||||
if f[14] or f[15]:
|
||||
continue
|
||||
am = (int(f[12][:4]), int(f[12][5:7]))
|
||||
for d in subir(f[2]):
|
||||
dirs[d][2][am] += 1
|
||||
revuelto = _TOCADO.get(raiz.lower(), 0) != movimientos_antes
|
||||
if not revuelto or vuelta == 2:
|
||||
break
|
||||
filas.clear()
|
||||
fallos.clear()
|
||||
dirs.clear()
|
||||
prog.update(n=0, bytes=0, aviso="se movieron archivos mientras se escaneaba: se vuelve a recorrer")
|
||||
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
|
||||
@@ -679,6 +692,8 @@ def escanear(ruta):
|
||||
prog["estado"] = "listo"
|
||||
if fallos:
|
||||
prog["error"] = f"{len(fallos)} sitios no se pudieron leer (el primero: {fallos[0]})"
|
||||
elif revuelto:
|
||||
prog["error"] = "se siguieron moviendo archivos mientras se escaneaba: vuelve a escanear cuando acabe"
|
||||
try:
|
||||
reubicar_trabajos(raiz) # la cola encuentra los clips que se movieron desde el último escaneo
|
||||
except Exception: # noqa
|
||||
@@ -2280,8 +2295,18 @@ class Plan:
|
||||
# Aplicar el plan (con bitácora y deshacer)
|
||||
# ==========================================================================
|
||||
|
||||
# carpeta de cliente (en minúsculas) -> cuántas veces ha movido la app algo de ella: el escaneo lo mira (un
|
||||
# contador y no la hora: en Windows dos momentos seguidos pueden dar la misma)
|
||||
_TOCADO = {}
|
||||
|
||||
|
||||
def bitacora(entrada):
|
||||
entrada["ts"] = time.time()
|
||||
if entrada.get("op") in ("mover", "rmdir", "mkdir"):
|
||||
for k in ("de", "a"):
|
||||
r = _raiz_de(entrada[k]) if entrada.get(k) else None
|
||||
if r:
|
||||
_TOCADO[r.lower()] = _TOCADO.get(r.lower(), 0) + 1
|
||||
with open(BITACORA_PATH, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entrada, ensure_ascii=False) + "\n")
|
||||
|
||||
@@ -2325,10 +2350,50 @@ def _renombrar(origen, destino, intentos=4):
|
||||
time.sleep(0.3 * (i + 1))
|
||||
|
||||
|
||||
# carpeta (en minúsculas) -> cómo está escrita en el disco; vale dos minutos, y se olvida al mover una carpeta
|
||||
_MAYUS = {"t": 0.0, "d": {}}
|
||||
|
||||
|
||||
def _olvidar_mayusculas():
|
||||
_MAYUS["d"].clear()
|
||||
_MAYUS["t"] = time.time()
|
||||
|
||||
|
||||
def _como_en_disco(d):
|
||||
"""La carpeta `d` escrita con las mayúsculas que tiene en el disco (si existe; si no, como viene). Windows no
|
||||
distingue «Altas Print» de «ALTAS PRINT»: si el plan mueve a la primera y en el disco está la segunda, el
|
||||
archivo cae en la que había, pero el inventario apuntaba la ruta del plan. Medido el 14-oct-2026 en Vía
|
||||
Sumapaz: 1.039 fotos con la carpeta mal escrita en el inventario, y la vista de carpetas enseñaba «Altas
|
||||
print», «Altas Print» y «ALTAS PRINT» donde en el disco hay una sola. Se pregunta al disco una vez por
|
||||
carpeta, desde la carpeta de cliente hacia abajo."""
|
||||
if time.time() - _MAYUS["t"] > 120:
|
||||
_olvidar_mayusculas()
|
||||
cache = _MAYUS["d"]
|
||||
k = d.lower()
|
||||
if k in cache:
|
||||
return cache[k]
|
||||
raiz = _raiz_de(d)
|
||||
padre, nombre = os.path.split(d)
|
||||
if not raiz or k == raiz.lower() or not nombre or padre == d:
|
||||
real = raiz if raiz and k == raiz.lower() else d
|
||||
else:
|
||||
arriba = _como_en_disco(padre)
|
||||
try:
|
||||
escritos = {n.lower(): n for n in os.listdir(arriba)}
|
||||
except OSError:
|
||||
escritos = {}
|
||||
real = os.path.join(arriba, escritos.get(nombre.lower(), nombre))
|
||||
cache[k] = real
|
||||
return real
|
||||
|
||||
|
||||
def mover(origen, destino, lote, movidos):
|
||||
"""mueve archivo o carpeta; si la carpeta destino existe, fusiona"""
|
||||
origen = os.path.normpath(origen)
|
||||
destino = os.path.normpath(destino)
|
||||
if origen == destino:
|
||||
return
|
||||
destino = os.path.join(_como_en_disco(os.path.dirname(destino)), os.path.basename(destino))
|
||||
if origen == destino:
|
||||
return
|
||||
if origen.lower() == destino.lower():
|
||||
@@ -2337,10 +2402,12 @@ def mover(origen, destino, lote, movidos):
|
||||
# carpeta consigo misma. Antes se devolvía sin hacer nada y el plan prometía un renombrado que
|
||||
# nunca ocurría: os.rename sí sabe hacerlo en NTFS.
|
||||
_renombrar(origen, destino)
|
||||
_olvidar_mayusculas()
|
||||
bitacora({"lote": lote, "op": "mover", "de": origen, "a": destino})
|
||||
movidos.append((origen, destino))
|
||||
return
|
||||
if os.path.isdir(origen) and os.path.isdir(destino):
|
||||
es_dir = os.path.isdir(origen)
|
||||
if es_dir and os.path.isdir(destino):
|
||||
# fusión: cada hijo va por su cuenta. Si uno tropieza (ya existe con ese nombre, está abierto),
|
||||
# los demás siguen; antes el primero que fallaba cortaba el bucle y la carpeta quedaba medio
|
||||
# vaciada, con el aviso hablando de un solo archivo.
|
||||
@@ -2382,6 +2449,8 @@ def mover(origen, destino, lote, movidos):
|
||||
except OSError as ex:
|
||||
os.remove(destino)
|
||||
raise OSError(f"«{os.path.basename(origen)}» está en uso (¿abierto en Resolve o Premiere?); no se mueve") from ex
|
||||
if es_dir:
|
||||
_olvidar_mayusculas() # lo que había debajo de la carpeta movida ya no está ahí
|
||||
bitacora({"lote": lote, "op": "mover", "de": origen, "a": destino})
|
||||
movidos.append((origen, destino))
|
||||
|
||||
@@ -2524,7 +2593,7 @@ def actualizar_inventario(movidos):
|
||||
# más de diez minutos en los que «Buscar clips» se rendía con «database is locked». Lo que hay dentro de
|
||||
# cada carpeta se mira en el inventario, por índice, movimiento a movimiento (no una vez al principio: una
|
||||
# carpeta puede aparecer a mitad de la tanda, renombrada por un movimiento anterior).
|
||||
tocadas = _tocadas(movidos)
|
||||
tocadas = _tocadas(_rutas_finales(movidos))
|
||||
existe = _existen({d for d, _ in tocadas})
|
||||
cola = _cola_tras_mover(con, movidos, marcas)
|
||||
with con:
|
||||
@@ -2644,8 +2713,8 @@ def reubicar_trabajos(raiz=None, ids=None):
|
||||
for r in con.execute(f"SELECT ruta, raiz, nombre, bytes FROM archivos WHERE es_basura = 0 AND nombre IN ({','.join('?' * len(parte))})", parte):
|
||||
if os.sep + R.DUPLICADOS + os.sep not in r["ruta"]:
|
||||
donde[((r["raiz"] or "").lower(), r["nombre"], r["bytes"])].append(r["ruta"])
|
||||
ocupadas = {(r["ruta"], r["formato"] or "h265_10bit") for r in con.execute(
|
||||
"SELECT ruta, formato FROM trabajos WHERE estado IN ('pendiente','corriendo','listo','verificado','reemplazado')")}
|
||||
ocupadas = {(r["ruta"], r["formato"] or "h265_10bit"): (r["id"], r["estado"]) for r in con.execute(
|
||||
"SELECT id, ruta, formato, estado FROM trabajos WHERE estado IN ('pendiente','corriendo','listo','verificado','reemplazado')")}
|
||||
n = 0
|
||||
with con:
|
||||
for t in sin_clip:
|
||||
@@ -2653,10 +2722,24 @@ def reubicar_trabajos(raiz=None, ids=None):
|
||||
if not sitio or len(sitio) != 1:
|
||||
continue
|
||||
nueva, fmt = sitio[0], t["formato"] or "h265_10bit"
|
||||
if (nueva, fmt) in ocupadas and t["estado"] in ("pendiente", "error", "cancelado"):
|
||||
otro = ocupadas.get((nueva, fmt))
|
||||
if otro and t["estado"] in ("pendiente", "error", "cancelado"):
|
||||
con.execute("DELETE FROM trabajos WHERE id = ?", (t["id"],))
|
||||
continue
|
||||
if (nueva, fmt) in ocupadas:
|
||||
if otro and t["estado"] in ("listo", "verificado"):
|
||||
# Ya estaba convertido y la 3.7, que no lo reconocía en su sitio nuevo, lo dejó encolar otra vez.
|
||||
# Medido el 14-oct-2026 en Línea Gene: los 11 verificados se volvieron a encolar con los 136 que
|
||||
# faltaban, y uno ya se había convertido dos veces. Si el nuevo aún espera, sobra él; si ya se
|
||||
# hizo, sobra este, el de la carpeta vieja (lo aparta «Limpiar terminados»).
|
||||
if otro[1] == "pendiente":
|
||||
con.execute("DELETE FROM trabajos WHERE id = ?", (otro[0],))
|
||||
else:
|
||||
con.execute("UPDATE trabajos SET estado = 'descartado', log = ? WHERE id = ?",
|
||||
(f"el clip se volvió a convertir en su sitio nuevo (#{otro[0]}): este convertido, en la carpeta "
|
||||
"vieja, sobra. «Limpiar terminados» lo aparta a _Para borrar", t["id"]))
|
||||
n += 1
|
||||
continue
|
||||
elif otro:
|
||||
continue
|
||||
c = {"ruta": nueva}
|
||||
if t["estado"] in ("pendiente", "error", "cancelado"):
|
||||
@@ -2667,7 +2750,7 @@ def reubicar_trabajos(raiz=None, ids=None):
|
||||
c["log"] = f"el clip se había movido; ahora está en «{os.path.relpath(os.path.dirname(nueva), t['raiz'])}» y se convierte allí"
|
||||
con.execute(f"UPDATE trabajos SET {', '.join(k + ' = ?' for k in c)} WHERE id = ?", (*c.values(), t["id"]))
|
||||
if t["estado"] in ("pendiente", "error", "listo", "verificado"):
|
||||
ocupadas.add((nueva, fmt))
|
||||
ocupadas[(nueva, fmt)] = (t["id"], "pendiente" if t["estado"] == "error" else t["estado"])
|
||||
n += 1
|
||||
for t in sin_salida:
|
||||
sitio = donde.get(((t["raiz"] or "").lower(), os.path.basename(t["salida"] or ""), t["bytes_out"]))
|
||||
@@ -2705,6 +2788,32 @@ def _son_archivos(rutas):
|
||||
return dict(zip(rutas, ex.map(os.path.isfile, rutas)))
|
||||
|
||||
|
||||
def _rutas_finales(movidos):
|
||||
"""Los movimientos de una tanda con cada ruta escrita como quedó al final: uno posterior pudo mover o renombrar
|
||||
la carpeta en la que estaba (el plan mete archivos en «10- octubre» y después la renombra a «10- Octubre»).
|
||||
Para recontar las carpetas hay que mirarlas al final: Windows da por existente «10- octubre» aunque ya se
|
||||
llame «10- Octubre», y el inventario acababa con las dos (y «Colores» pintaba dos veces la misma)."""
|
||||
despues = {} # lo que un movimiento posterior se llevó -> dónde acabó
|
||||
|
||||
def final(p):
|
||||
q, resto = p, ""
|
||||
while True:
|
||||
if q in despues:
|
||||
return despues[q] + resto
|
||||
padre = os.path.dirname(q)
|
||||
if padre == q:
|
||||
return p
|
||||
resto = q[len(padre):] + resto
|
||||
q = padre
|
||||
out = []
|
||||
for de, a in reversed(movidos):
|
||||
fa = final(a)
|
||||
out.append((os.path.join(final(os.path.dirname(de)), os.path.basename(de)), fa))
|
||||
despues[de] = fa
|
||||
out.reverse()
|
||||
return out
|
||||
|
||||
|
||||
def _tocadas(movidos):
|
||||
"""Las carpetas cuyo recuento cambia con estos movimientos: las de origen y destino y todas las de encima. El
|
||||
destino mismo entra siempre: si era un archivo, el disco dirá que no es una carpeta y no se apunta nada."""
|
||||
|
||||
Referencia en una nueva incidencia
Block a user