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:
21
README.md
21
README.md
@@ -932,7 +932,26 @@ se convierta, la idea es aprovechar lo máximo el espacio».
|
|||||||
- El descarte después de convertir sigue al mínimo: se descarta lo que ahorre menos del mínimo menos 10 puntos
|
- El descarte después de convertir sigue al mínimo: se descarta lo que ahorre menos del mínimo menos 10 puntos
|
||||||
(con el 30 %, menos del 20 %, como hasta ahora).
|
(con el 30 %, menos del 20 %, como hasta ahora).
|
||||||
|
|
||||||
Pruebas: `prueba_orden.py` 39 casos (el de «lo ya comprimido» pasa a ser el del 30 %: H.265 holgado y justo, H.264
|
Y lo que salió al cotejar, archivo por archivo, cada disco con el inventario (Munka: «quiero saber si están todos
|
||||||
|
los archivos del disco, si hizo bien su escaneo»). El escaneo estaba completo: en los cinco clientes no faltaba
|
||||||
|
ningún archivo que existiera al escanear (lo que no estaba eran los convertidos hechos después, y en Diana Méndez
|
||||||
|
lo que alguien vació de _Para borrar). Pero había tres cosas:
|
||||||
|
|
||||||
|
- **Las mayúsculas de las carpetas.** Windows no distingue «Altas Print» de «ALTAS PRINT». Si el plan movía a la
|
||||||
|
primera y en el disco estaba la segunda, el archivo caía en la que había, pero el inventario apuntaba la ruta
|
||||||
|
del plan: en Vía Sumapaz, 1.039 fotos con la carpeta mal escrita, y «Altas print», «Altas Print» y «ALTAS
|
||||||
|
PRINT» como tres carpetas donde en el disco hay una. Ahora se escribe la carpeta como está en el disco, y las
|
||||||
|
carpetas se recuentan donde quedaron al final de la tanda.
|
||||||
|
- **Mover mientras se escanea.** Un proxy que la app apartó a _Para borrar a las 10:02, en pleno escaneo de Vía
|
||||||
|
Sumapaz, quedó dos veces en el inventario. Ahora, si la app mueve algo de una carpeta mientras se recorre, se
|
||||||
|
recorre otra vez (hasta tres; si sigue moviéndose, lo avisa).
|
||||||
|
- **Lo ya convertido, dos veces.** Con la 3.7 los 11 verificados de Línea Gene se volvieron a encolar en su sitio
|
||||||
|
nuevo junto con los 136 que faltaban, y uno ya se había convertido otra vez. Ahora, al reengancharlos, la
|
||||||
|
conversión que aún espera se quita; la que ya se hizo deja «no compensa» a la vieja, y «Limpiar terminados»
|
||||||
|
aparta su archivo.
|
||||||
|
|
||||||
|
Pruebas: `prueba_orden.py` 42 casos (tres nuevos: las mayúsculas del disco, escanear mientras se mueve, y lo ya
|
||||||
|
convertido que se volvió a encolar; el de «lo ya comprimido» pasa a ser el del 30 %: H.265 holgado y justo, H.264
|
||||||
por debajo del objetivo, AV1 de móvil, Duplicados, el ajuste al 45 % y el descarte); `prueba_funcional.py` 33 de
|
por debajo del objetivo, AV1 de móvil, Duplicados, el ajuste al 45 % y el descarte); `prueba_funcional.py` 33 de
|
||||||
33, `probar.py` 19 y `prueba_ventana.py` 18.
|
33, `probar.py` 19 y `prueba_ventana.py` 18.
|
||||||
|
|
||||||
|
|||||||
147
organizador.py
147
organizador.py
@@ -615,18 +615,31 @@ def escanear(ruta):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
walk(raiz, False, False, False, "", "raiz", None, None)
|
# Si mientras se recorre la carpeta la app mueve algo de ella («Aplicar», apartar, la cola), lo recogido
|
||||||
prog["fallos"] = len(fallos)
|
# mezcla el antes y el después: un archivo visto en su sitio y luego otra vez en la papelera, o en
|
||||||
_fechas_de_rodaje(filas)
|
# ninguno. Medido el 14-oct-2026 en Vía Sumapaz: un proxy apartado a las 10:02, en pleno escaneo, quedó
|
||||||
_heredar_fechas(filas)
|
# dos veces en el inventario. Entonces se vuelve a recorrer (hasta tres veces).
|
||||||
# de qué mes es cada carpeta: se cuenta al final, con las fechas ya corregidas, y sin la basura
|
for vuelta in range(3):
|
||||||
# ni los proxies (un .DS_Store de otro año no dice nada del material)
|
movimientos_antes = _TOCADO.get(raiz.lower(), 0)
|
||||||
for f in filas:
|
walk(raiz, False, False, False, "", "raiz", None, None)
|
||||||
if f[14] or f[15]:
|
prog["fallos"] = len(fallos)
|
||||||
continue
|
_fechas_de_rodaje(filas)
|
||||||
am = (int(f[12][:4]), int(f[12][5:7]))
|
_heredar_fechas(filas)
|
||||||
for d in subir(f[2]):
|
# de qué mes es cada carpeta: se cuenta al final, con las fechas ya corregidas, y sin la basura
|
||||||
dirs[d][2][am] += 1
|
# 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()
|
con = db()
|
||||||
# Antes se borraba el inventario y se guardaba lo recogido pasara lo que pasara. Si una unidad de
|
# 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
|
# 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"
|
prog["estado"] = "listo"
|
||||||
if fallos:
|
if fallos:
|
||||||
prog["error"] = f"{len(fallos)} sitios no se pudieron leer (el primero: {fallos[0]})"
|
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:
|
try:
|
||||||
reubicar_trabajos(raiz) # la cola encuentra los clips que se movieron desde el último escaneo
|
reubicar_trabajos(raiz) # la cola encuentra los clips que se movieron desde el último escaneo
|
||||||
except Exception: # noqa
|
except Exception: # noqa
|
||||||
@@ -2280,8 +2295,18 @@ class Plan:
|
|||||||
# Aplicar el plan (con bitácora y deshacer)
|
# 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):
|
def bitacora(entrada):
|
||||||
entrada["ts"] = time.time()
|
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:
|
with open(BITACORA_PATH, "a", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(entrada, ensure_ascii=False) + "\n")
|
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))
|
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):
|
def mover(origen, destino, lote, movidos):
|
||||||
"""mueve archivo o carpeta; si la carpeta destino existe, fusiona"""
|
"""mueve archivo o carpeta; si la carpeta destino existe, fusiona"""
|
||||||
origen = os.path.normpath(origen)
|
origen = os.path.normpath(origen)
|
||||||
destino = os.path.normpath(destino)
|
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:
|
if origen == destino:
|
||||||
return
|
return
|
||||||
if origen.lower() == destino.lower():
|
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
|
# 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.
|
# nunca ocurría: os.rename sí sabe hacerlo en NTFS.
|
||||||
_renombrar(origen, destino)
|
_renombrar(origen, destino)
|
||||||
|
_olvidar_mayusculas()
|
||||||
bitacora({"lote": lote, "op": "mover", "de": origen, "a": destino})
|
bitacora({"lote": lote, "op": "mover", "de": origen, "a": destino})
|
||||||
movidos.append((origen, destino))
|
movidos.append((origen, destino))
|
||||||
return
|
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),
|
# 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
|
# 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.
|
# vaciada, con el aviso hablando de un solo archivo.
|
||||||
@@ -2382,6 +2449,8 @@ def mover(origen, destino, lote, movidos):
|
|||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
os.remove(destino)
|
os.remove(destino)
|
||||||
raise OSError(f"«{os.path.basename(origen)}» está en uso (¿abierto en Resolve o Premiere?); no se mueve") from ex
|
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})
|
bitacora({"lote": lote, "op": "mover", "de": origen, "a": destino})
|
||||||
movidos.append((origen, 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
|
# 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
|
# 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).
|
# 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})
|
existe = _existen({d for d, _ in tocadas})
|
||||||
cola = _cola_tras_mover(con, movidos, marcas)
|
cola = _cola_tras_mover(con, movidos, marcas)
|
||||||
with con:
|
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):
|
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"]:
|
if os.sep + R.DUPLICADOS + os.sep not in r["ruta"]:
|
||||||
donde[((r["raiz"] or "").lower(), r["nombre"], r["bytes"])].append(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(
|
ocupadas = {(r["ruta"], r["formato"] or "h265_10bit"): (r["id"], r["estado"]) for r in con.execute(
|
||||||
"SELECT ruta, formato FROM trabajos WHERE estado IN ('pendiente','corriendo','listo','verificado','reemplazado')")}
|
"SELECT id, ruta, formato, estado FROM trabajos WHERE estado IN ('pendiente','corriendo','listo','verificado','reemplazado')")}
|
||||||
n = 0
|
n = 0
|
||||||
with con:
|
with con:
|
||||||
for t in sin_clip:
|
for t in sin_clip:
|
||||||
@@ -2653,10 +2722,24 @@ def reubicar_trabajos(raiz=None, ids=None):
|
|||||||
if not sitio or len(sitio) != 1:
|
if not sitio or len(sitio) != 1:
|
||||||
continue
|
continue
|
||||||
nueva, fmt = sitio[0], t["formato"] or "h265_10bit"
|
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"],))
|
con.execute("DELETE FROM trabajos WHERE id = ?", (t["id"],))
|
||||||
continue
|
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
|
continue
|
||||||
c = {"ruta": nueva}
|
c = {"ruta": nueva}
|
||||||
if t["estado"] in ("pendiente", "error", "cancelado"):
|
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í"
|
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"]))
|
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"):
|
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
|
n += 1
|
||||||
for t in sin_salida:
|
for t in sin_salida:
|
||||||
sitio = donde.get(((t["raiz"] or "").lower(), os.path.basename(t["salida"] or ""), t["bytes_out"]))
|
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)))
|
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):
|
def _tocadas(movidos):
|
||||||
"""Las carpetas cuyo recuento cambia con estos movimientos: las de origen y destino y todas las de encima. El
|
"""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."""
|
destino mismo entra siempre: si era un archivo, el disco dirá que no es una carpeta y no se apunta nada."""
|
||||||
|
|||||||
101
prueba_orden.py
101
prueba_orden.py
@@ -501,6 +501,60 @@ class Motor(unittest.TestCase):
|
|||||||
self.assertEqual(R.firma_archivo("C0001_arch.mp4"), "SONY")
|
self.assertEqual(R.firma_archivo("C0001_arch.mp4"), "SONY")
|
||||||
self.assertTrue(R.es_conversion("C0001_10bit.mp4")) # y los de siempre siguen valiendo
|
self.assertTrue(R.es_conversion("C0001_10bit.mp4")) # y los de siempre siguen valiendo
|
||||||
|
|
||||||
|
def test_el_inventario_escribe_la_carpeta_como_el_disco(self):
|
||||||
|
"""Vía Sumapaz, 14-oct-2026: el plan movió fotos a «Altas Print» y «Altas print», que en el disco son la misma
|
||||||
|
«ALTAS PRINT»; el inventario apuntó tres carpetas distintas con 1.039 fotos mal escritas."""
|
||||||
|
raiz = cliente("Mayusculas")
|
||||||
|
nr = os.path.normpath(raiz)
|
||||||
|
a = mk(raiz, "Sueltas/foto1.jpg", 2024, 5, 3)
|
||||||
|
b = mk(raiz, "Sueltas/foto2.jpg", 2024, 5, 3)
|
||||||
|
mk(raiz, "2024/05- Mayo/_Fotos/ALTAS PRINT/foto0.jpg", 2024, 5, 3)
|
||||||
|
O.escanear(raiz)
|
||||||
|
movidos = []
|
||||||
|
O.mover(a, os.path.join(nr, "2024", "05- Mayo", "_Fotos", "Altas Print", "foto1.jpg"), "prueba", movidos)
|
||||||
|
O.mover(b, os.path.join(nr, "2024", "05- mayo", "_fotos", "altas print", "foto2.jpg"), "prueba", movidos)
|
||||||
|
bien = os.path.join(nr, "2024", "05- Mayo", "_Fotos", "ALTAS PRINT")
|
||||||
|
self.assertEqual([d for _, d in movidos], [os.path.join(bien, "foto1.jpg"), os.path.join(bien, "foto2.jpg")])
|
||||||
|
O.actualizar_inventario(movidos)
|
||||||
|
con = O.db()
|
||||||
|
rutas = {r[0] for r in con.execute("SELECT ruta FROM archivos WHERE raiz = ?", (nr,))}
|
||||||
|
carpetas = [r[0] for r in con.execute("SELECT ruta FROM carpetas WHERE raiz = ? AND lower(ruta) = lower(?)", (nr, bien))]
|
||||||
|
con.close()
|
||||||
|
self.assertTrue({os.path.join(bien, n) for n in ("foto0.jpg", "foto1.jpg", "foto2.jpg")} <= rutas, rutas)
|
||||||
|
self.assertEqual(carpetas, [bien])
|
||||||
|
# una carpeta que no existía se crea como la escribe el plan, y las siguientes la siguen igual
|
||||||
|
c = mk(raiz, "Sueltas/foto3.jpg", 2024, 5, 3)
|
||||||
|
d = mk(raiz, "Sueltas/foto4.jpg", 2024, 5, 3)
|
||||||
|
movidos = []
|
||||||
|
O.mover(c, os.path.join(nr, "Nueva Carpeta", "foto3.jpg"), "prueba", movidos)
|
||||||
|
O.mover(d, os.path.join(nr, "NUEVA CARPETA", "foto4.jpg"), "prueba", movidos)
|
||||||
|
self.assertEqual({os.path.dirname(x) for _, x in movidos}, {os.path.join(nr, "Nueva Carpeta")})
|
||||||
|
|
||||||
|
def test_escaneo_con_movimientos_a_la_vez(self):
|
||||||
|
"""Vía Sumapaz, 14-oct-2026: un proxy apartado a _Para borrar en pleno escaneo quedó dos veces en el
|
||||||
|
inventario (visto en su sitio y luego en la papelera). Si la app mueve algo mientras se recorre, se recorre otra vez."""
|
||||||
|
raiz = cliente("EnMarcha")
|
||||||
|
nr = os.path.normpath(raiz)
|
||||||
|
p = mk(raiz, "2024/09- Septiembre/03- Botellitas/Proxy/Botellas Vr2.mov", 2024, 9, 3)
|
||||||
|
mk(raiz, "2024/09- Septiembre/03- Botellitas/Botellas Vr2.mov", 2024, 9, 3, tam=4096)
|
||||||
|
real = O._fechas_de_rodaje
|
||||||
|
llamadas = []
|
||||||
|
|
||||||
|
def a_la_vez(filas):
|
||||||
|
llamadas.append(len(filas))
|
||||||
|
if len(llamadas) == 1: # después de recorrer, antes de guardar: la app aparta el proxy
|
||||||
|
O.mover(p, os.path.join(nr, R.PAPELERA, "proxies", os.path.relpath(p, nr)), "aparte", [])
|
||||||
|
return real(filas)
|
||||||
|
O._fechas_de_rodaje = a_la_vez
|
||||||
|
self.addCleanup(setattr, O, "_fechas_de_rodaje", real)
|
||||||
|
O.escanear(raiz)
|
||||||
|
self.assertEqual(len(llamadas), 2)
|
||||||
|
con = O.db()
|
||||||
|
proxies = [r[0] for r in con.execute("SELECT ruta FROM archivos WHERE raiz = ? AND nombre = 'Botellas Vr2.mov' AND bytes = 2048", (nr,))]
|
||||||
|
con.close()
|
||||||
|
self.assertEqual(proxies, [os.path.join(nr, R.PAPELERA, "proxies", os.path.relpath(p, nr))])
|
||||||
|
self.assertEqual(O.PROGRESO[nr]["estado"], "listo")
|
||||||
|
|
||||||
def test_inventario_de_otra_version(self):
|
def test_inventario_de_otra_version(self):
|
||||||
"""Lo que apunta el escaneo cambió: un inventario viejo se relee antes de planear nada."""
|
"""Lo que apunta el escaneo cambió: un inventario viejo se relee antes de planear nada."""
|
||||||
raiz = cliente("Viejo")
|
raiz = cliente("Viejo")
|
||||||
@@ -1214,6 +1268,53 @@ class Convertidor(unittest.TestCase):
|
|||||||
self.assertTrue(os.path.isfile(os.path.join(nr, R.PAPELERA, "originales", "Otra carpeta", os.path.basename(c2))))
|
self.assertTrue(os.path.isfile(os.path.join(nr, R.PAPELERA, "originales", "Otra carpeta", os.path.basename(c2))))
|
||||||
self.assertEqual(self.trabajo(ic)["salida"], junto)
|
self.assertEqual(self.trabajo(ic)["salida"], junto)
|
||||||
|
|
||||||
|
def test_lo_ya_convertido_no_se_convierte_dos_veces(self):
|
||||||
|
"""Línea Gene, 14-oct-2026: con la 3.7 los 11 verificados se volvieron a encolar en su sitio nuevo, y uno ya
|
||||||
|
se había convertido otra vez. Al reengancharlos: la conversión que aún espera sobra; la ya hecha deja de más
|
||||||
|
la vieja."""
|
||||||
|
raiz = cliente("DosVeces")
|
||||||
|
nr = os.path.normpath(raiz)
|
||||||
|
a = mk(raiz, "Material Sony/20250722_Labstream3805.MP4", 2025, 7, 22, tam=300000)
|
||||||
|
b = mk(raiz, "Material Sony/20250722_Labstream3766.MP4", 2025, 7, 22, tam=300001)
|
||||||
|
O.escanear(raiz)
|
||||||
|
self.leidos(raiz, {os.path.basename(x): {"codec": "h264", "perfil": "High", "kbps": 120000} for x in (a, b)})
|
||||||
|
ia, ib = O.encolar([a, b], "equilibrado", "420", "rec709", nr)
|
||||||
|
for i in (ia, ib):
|
||||||
|
with open(self.trabajo(i)["salida"], "wb") as fh:
|
||||||
|
fh.write(b"x" * 90000)
|
||||||
|
O.set_trabajo(i, estado="verificado", bytes_out=90000)
|
||||||
|
# la 3.7: los clips se movieron, la cola no se enteró, y se volvieron a encolar en el sitio nuevo
|
||||||
|
nueva = os.path.join(nr, "2025", "07- Julio", "00_Grabacion", "22- Martes", "SONY")
|
||||||
|
os.makedirs(nueva)
|
||||||
|
a2, b2 = os.path.join(nueva, os.path.basename(a)), os.path.join(nueva, os.path.basename(b))
|
||||||
|
os.rename(a, a2)
|
||||||
|
os.rename(b, b2)
|
||||||
|
with open(os.path.splitext(a2)[0] + "_10bit.mp4", "wb") as fh: # el de a ya se convirtió otra vez allí
|
||||||
|
fh.write(b"y" * 90000)
|
||||||
|
con = O.db()
|
||||||
|
with con:
|
||||||
|
ids = []
|
||||||
|
for ruta, estado in ((a2, "verificado"), (b2, "pendiente")):
|
||||||
|
ids.append(con.execute("INSERT INTO trabajos (ruta, salida, motor, perfil, chroma, color, kbps, estado, bytes_in, bytes_out, creado, raiz, formato) "
|
||||||
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", (ruta, os.path.splitext(ruta)[0] + "_10bit.mp4", "ffmpeg", "equilibrado", "420",
|
||||||
|
"rec709", 21150, estado, os.path.getsize(ruta), 90000, time.time(), nr,
|
||||||
|
"h265_10bit")).lastrowid)
|
||||||
|
con.close()
|
||||||
|
O.escanear(raiz)
|
||||||
|
ta, tb = self.trabajo(ia), self.trabajo(ib)
|
||||||
|
# el de a ya se había vuelto a convertir: sobra el viejo, que queda «no compensa» con su porqué
|
||||||
|
self.assertEqual(ta["estado"], "descartado")
|
||||||
|
self.assertIn("se volvió a convertir en su sitio nuevo", ta["log"])
|
||||||
|
self.assertEqual(self.trabajo(ids[0])["estado"], "verificado")
|
||||||
|
# el de b aún esperaba: se quita de la cola, y el viejo se reengancha (se reemplaza junto a su original)
|
||||||
|
con = O.db()
|
||||||
|
self.assertIsNone(con.execute("SELECT id FROM trabajos WHERE id = ?", (ids[1],)).fetchone())
|
||||||
|
con.close()
|
||||||
|
self.assertEqual((tb["estado"], tb["ruta"]), ("verificado", b2))
|
||||||
|
r = O.reemplazar([ib])[0]
|
||||||
|
self.assertTrue(r["ok"], r)
|
||||||
|
self.assertTrue(os.path.isfile(os.path.join(nueva, os.path.basename(tb["salida"]))))
|
||||||
|
|
||||||
def test_el_codificador_que_toca(self):
|
def test_el_codificador_que_toca(self):
|
||||||
fm = O.FORMATOS["h265_10bit"]
|
fm = O.FORMATOS["h265_10bit"]
|
||||||
t = {"chroma": "420", "perfil": "equilibrado", "kbps": 30000}
|
t = {"chroma": "420", "perfil": "equilibrado", "kbps": 30000}
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user